From 3d9eb1e2c669974706896bfc47951c4529990afe Mon Sep 17 00:00:00 2001 From: venessa <854003762@qq.com> Date: Sun, 28 Apr 2024 12:43:07 +0800 Subject: [PATCH 01/41] feat: sbin use the generated zk conf (#3901) Co-authored-by: lijiangnan --- release/sbin/start-zks.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/sbin/start-zks.sh b/release/sbin/start-zks.sh index 775d52715ac..0d004cd15a5 100755 --- a/release/sbin/start-zks.sh +++ b/release/sbin/start-zks.sh @@ -32,7 +32,7 @@ do dir=$(echo "$line" | awk -F ' ' '{print $3}') echo "start zookeeper in $dir with endpoint $host:$port " - cmd="cd $dir && bin/zkServer.sh start" + cmd="cd $dir && bin/zkServer.sh start $dir/conf/zoo.cfg" # special for java pre="" if [[ -n $RUNNER_JAVA_HOME ]]; then From d9bb344d11354bb53d2f8ba7f77daf85e9c78917 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Sun, 28 Apr 2024 15:21:38 +0800 Subject: [PATCH 02/41] refactor!: relocate go sdk (#3889) * refactor!: relocate go sdk moving to https://github.com/4paradigm/openmldb-go-sdk * go readme * ci: fix sdk workflow --- .github/workflows/sdk.yml | 53 ++------------------------------ docs/en/quickstart/sdk/go_sdk.md | 4 +-- docs/zh/quickstart/sdk/go_sdk.md | 4 +-- go/README.md | 5 +++ 4 files changed, 12 insertions(+), 54 deletions(-) create mode 100644 go/README.md diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index eafe87cbe1f..482374b9cb1 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -368,49 +368,8 @@ jobs: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - go-sdk: - runs-on: ubuntu-latest - container: - image: ghcr.io/4paradigm/hybridsql:latest - env: - OPENMLDB_BUILD_TARGET: "openmldb" - OPENMLDB_MODE: standalone - steps: - - uses: actions/checkout@v2 - - - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - - name: build openmldb - run: make build install - - - name: start server - run: ./openmldb/sbin/start-all.sh - - - name: init test database - env: - OPENMLDB_NS_HOST: 127.0.0.1 - OPENMLDB_NS_PORT: 6527 - run: | - echo "CREATE DATABASE test_db;" | ./openmldb/bin/openmldb --host=$OPENMLDB_NS_HOST --port=$OPENMLDB_NS_PORT - - - name: go test - env: - OPENMLDB_APISERVER_HOST: 127.0.0.1 - OPENMLDB_APISERVER_PORT: 8080 - working-directory: go - run: go test ./... -race -covermode=atomic -coverprofile=coverage.out - - - name: upload coverage - uses: actions/upload-artifact@v3 - with: - name: coverage-go-report-${{ github.sha }} - path: go/coverage.out - retention-days: 3 - publish-test-results: - needs: ["java-sdk", "python-sdk", "go-sdk"] + needs: ["java-sdk", "python-sdk"] # the action will only run on 4paradigm/OpenMLDB's context, not for fork repo or dependabot if: > always() && github.event_name == 'push' || ( @@ -426,7 +385,7 @@ jobs: comment_title: SDK Test Report publish-coverage-results: - needs: ["java-sdk", "python-sdk", "go-sdk"] + needs: ["java-sdk", "python-sdk"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -445,16 +404,10 @@ jobs: name: coverage-python-report-${{ github.sha }} path: python - - name: Download Artifacts (go) - uses: actions/download-artifact@v3 - with: - name: coverage-go-report-${{ github.sha }} - path: go - - name: Upload Coverage Report uses: codecov/codecov-action@v4 with: - files: go/coverage.out,python/openmldb_sdk/tests/coverage.xml,python/openmldb_tool/tests/coverage.xml,java/**/target/site/jacoco/jacoco.xml,java/**/target/scoverage.xml + files: python/openmldb_sdk/tests/coverage.xml,python/openmldb_tool/tests/coverage.xml,java/**/target/site/jacoco/jacoco.xml,java/**/target/scoverage.xml name: coverage-sdk token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/docs/en/quickstart/sdk/go_sdk.md b/docs/en/quickstart/sdk/go_sdk.md index 4c07120a932..08b5ed856ec 100644 --- a/docs/en/quickstart/sdk/go_sdk.md +++ b/docs/en/quickstart/sdk/go_sdk.md @@ -11,7 +11,7 @@ The current functionality support of the Go SDK is not yet complete. It is curre ## Go SDK installation ```bash -go get github.com/4paradigm/OpenMLDB/go +go get github.com/4paradigm/openmldb-go-sdk ``` ## Go SDK usage @@ -79,7 +79,7 @@ import ( "database/sql" // Load OpenMLDB SDK - _ "github.com/4paradigm/OpenMLDB/go" + _ "github.com/4paradigm/openmldb-go-sdk ) func main() { diff --git a/docs/zh/quickstart/sdk/go_sdk.md b/docs/zh/quickstart/sdk/go_sdk.md index 140cf17d69a..a5b88ab5516 100644 --- a/docs/zh/quickstart/sdk/go_sdk.md +++ b/docs/zh/quickstart/sdk/go_sdk.md @@ -12,7 +12,7 @@ Go SDK 目前功能支持上并不完善,目前仅用于开发测试或者特 ## Go SDK 包安装 ```Bash -go get github.com/4paradigm/OpenMLDB/go +go get github.com/4paradigm/openmldb-go-sdk ``` ## 使用 Go SDK @@ -80,7 +80,7 @@ import ( "database/sql" // 加载 OpenMLDB SDK - _ "github.com/4paradigm/OpenMLDB/go" + _ "github.com/4paradigm/openmldb-go-sdk" ) func main() { diff --git a/go/README.md b/go/README.md new file mode 100644 index 00000000000..be17cb2aa9e --- /dev/null +++ b/go/README.md @@ -0,0 +1,5 @@ +**Moved to https://github.com/4paradigm/openmldb-go-sdk** + +Please note this directory only reserved for backwards compatibility use over `github.com/4paradigm/OpenMLDB/go`, and will removed anytime in later releases. + +Consider migrate to `github.com/4paradigm/openmldb-go-sdk`. From 1632b3ac5bfe757fa18dc304f3dee8c78cb1e7f0 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Mon, 6 May 2024 19:09:35 +0800 Subject: [PATCH 03/41] docs: fix example (#3907) raw SQL request mode example was wrong because execute_mode should be request --- docs/en/openmldb_sql/dql/SELECT_STATEMENT.md | 2 +- docs/zh/openmldb_sql/dql/SELECT_STATEMENT.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/openmldb_sql/dql/SELECT_STATEMENT.md b/docs/en/openmldb_sql/dql/SELECT_STATEMENT.md index 71e3032a57a..6aa11cf7d1a 100644 --- a/docs/en/openmldb_sql/dql/SELECT_STATEMENT.md +++ b/docs/en/openmldb_sql/dql/SELECT_STATEMENT.md @@ -142,6 +142,6 @@ Parentheses `()` expression is the minimal unit to a request row, every expressi -- executing SQL as request mode, with request row (10, "foo", timestamp(4000)) SELECT id, count (val) over (partition by id order by ts rows between 10 preceding and current row) FROM t1 -CONFIG (execute_mode = 'online', values = (10, "foo", timestamp (4000))) +CONFIG (execute_mode = 'request', values = (10, "foo", timestamp (4000))) ``` diff --git a/docs/zh/openmldb_sql/dql/SELECT_STATEMENT.md b/docs/zh/openmldb_sql/dql/SELECT_STATEMENT.md index d26d1c9fd96..4092c0c69c6 100644 --- a/docs/zh/openmldb_sql/dql/SELECT_STATEMENT.md +++ b/docs/zh/openmldb_sql/dql/SELECT_STATEMENT.md @@ -153,7 +153,7 @@ OpenMLDB >= 0.9.0 支持在 query statement 中用 CONFIG 子句配置 SQL 的 -- 执行请求行为 (10, "foo", timestamp(4000)) 的在线请求模式 query SELECT id, count (val) over (partition by id order by ts rows between 10 preceding and current row) FROM t1 -CONFIG (execute_mode = 'online', values = (10, "foo", timestamp (4000))) +CONFIG (execute_mode = 'request', values = (10, "foo", timestamp (4000))) ``` ## 离线同步模式 Query From 8ce7d727117164c292a4eed819d103a7dde32e71 Mon Sep 17 00:00:00 2001 From: oh2024 <162292688+oh2024@users.noreply.github.com> Date: Tue, 7 May 2024 18:03:42 +0800 Subject: [PATCH 04/41] fix: make clients use always send auth info (#3906) * fix: make clients use auth by default * fix: let skip auth flag only affect verify --- src/auth/brpc_authenticator.cc | 4 ++++ src/cmd/openmldb.cc | 29 ++++++++++++----------------- src/nameserver/name_server_impl.cc | 12 +++++------- src/rpc/rpc_client.h | 4 +--- src/sdk/mini_cluster.h | 1 - src/tablet/file_sender.cc | 4 +--- 6 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/auth/brpc_authenticator.cc b/src/auth/brpc_authenticator.cc index f1964334c3f..4a56ebf0165 100644 --- a/src/auth/brpc_authenticator.cc +++ b/src/auth/brpc_authenticator.cc @@ -18,6 +18,7 @@ #include "auth_utils.h" #include "butil/endpoint.h" +#include "nameserver/system_table.h" namespace openmldb::authn { @@ -37,6 +38,9 @@ int BRPCAuthenticator::GenerateCredential(std::string* auth_str) const { int BRPCAuthenticator::VerifyCredential(const std::string& auth_str, const butil::EndPoint& client_addr, brpc::AuthContext* out_ctx) const { + if (FLAGS_skip_grant_tables) { + return 0; + } if (auth_str.length() < 2) { return -1; } diff --git a/src/cmd/openmldb.cc b/src/cmd/openmldb.cc index 0f1bd920d2b..74017d790fa 100644 --- a/src/cmd/openmldb.cc +++ b/src/cmd/openmldb.cc @@ -149,15 +149,12 @@ void StartNameServer() { brpc::ServerOptions options; std::unique_ptr user_access_manager; std::unique_ptr server_authenticator; - if (!FLAGS_skip_grant_tables) { - user_access_manager = - std::make_unique(name_server->GetSystemTableIterator()); - server_authenticator = std::make_unique( - [&user_access_manager](const std::string& host, const std::string& username, const std::string& password) { - return user_access_manager->IsAuthenticated(host, username, password); - }); - options.auth = server_authenticator.get(); - } + user_access_manager = std::make_unique(name_server->GetSystemTableIterator()); + server_authenticator = std::make_unique( + [&user_access_manager](const std::string& host, const std::string& username, const std::string& password) { + return user_access_manager->IsAuthenticated(host, username, password); + }); + options.auth = server_authenticator.get(); options.num_threads = FLAGS_thread_pool_size; brpc::Server server; @@ -259,14 +256,12 @@ void StartTablet() { std::unique_ptr user_access_manager; std::unique_ptr server_authenticator; - if (!FLAGS_skip_grant_tables) { - user_access_manager = std::make_unique(tablet->GetSystemTableIterator()); - server_authenticator = std::make_unique( - [&user_access_manager](const std::string& host, const std::string& username, const std::string& password) { - return user_access_manager->IsAuthenticated(host, username, password); - }); - options.auth = server_authenticator.get(); - } + user_access_manager = std::make_unique(tablet->GetSystemTableIterator()); + server_authenticator = std::make_unique( + [&user_access_manager](const std::string& host, const std::string& username, const std::string& password) { + return user_access_manager->IsAuthenticated(host, username, password); + }); + options.auth = server_authenticator.get(); options.num_threads = FLAGS_thread_pool_size; brpc::Server server; if (server.AddService(tablet, brpc::SERVER_DOESNT_OWN_SERVICE) != 0) { diff --git a/src/nameserver/name_server_impl.cc b/src/nameserver/name_server_impl.cc index 86b0fb47b21..ff1db103a29 100644 --- a/src/nameserver/name_server_impl.cc +++ b/src/nameserver/name_server_impl.cc @@ -1520,12 +1520,10 @@ bool NameServerImpl::Init(const std::string& zk_cluster, const std::string& zk_p task_vec_.resize(FLAGS_name_server_task_max_concurrency + FLAGS_name_server_task_concurrency_for_replica_cluster); task_thread_pool_.DelayTask(FLAGS_make_snapshot_check_interval, boost::bind(&NameServerImpl::SchedMakeSnapshot, this)); - if (!FLAGS_skip_grant_tables) { - std::shared_ptr<::openmldb::nameserver::TableInfo> table_info; - while ( - !GetTableInfo(::openmldb::nameserver::USER_INFO_NAME, ::openmldb::nameserver::INTERNAL_DB, &table_info)) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } + std::shared_ptr<::openmldb::nameserver::TableInfo> table_info; + while ( + !GetTableInfo(::openmldb::nameserver::USER_INFO_NAME, ::openmldb::nameserver::INTERNAL_DB, &table_info)) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } return true; } @@ -5593,7 +5591,7 @@ void NameServerImpl::OnLocked() { PDLOG(WARNING, "recover failed"); } CreateDatabaseOrExit(INTERNAL_DB); - if (!FLAGS_skip_grant_tables && db_table_info_[INTERNAL_DB].count(USER_INFO_NAME) == 0) { + if (db_table_info_[INTERNAL_DB].count(USER_INFO_NAME) == 0) { auto temp = FLAGS_system_table_replica_num; FLAGS_system_table_replica_num = tablets_.size(); CreateSystemTableOrExit(SystemTableType::kUser); diff --git a/src/rpc/rpc_client.h b/src/rpc/rpc_client.h index 0fbfdf757fa..92e27279891 100644 --- a/src/rpc/rpc_client.h +++ b/src/rpc/rpc_client.h @@ -104,9 +104,7 @@ class RpcClient { if (use_sleep_policy_) { options.retry_policy = &sleep_retry_policy; } - if (!FLAGS_skip_grant_tables) { - options.auth = &client_authenticator_; - } + options.auth = &client_authenticator_; if (channel_->Init(endpoint_.c_str(), "", &options) != 0) { return -1; diff --git a/src/sdk/mini_cluster.h b/src/sdk/mini_cluster.h index 2851e111cab..673e1cb1f61 100644 --- a/src/sdk/mini_cluster.h +++ b/src/sdk/mini_cluster.h @@ -365,7 +365,6 @@ class StandaloneEnv { }); brpc::ServerOptions options; options.auth = ns_authenticator_; - options.auth = ns_authenticator_; if (ns_.AddService(nameserver, brpc::SERVER_OWNS_SERVICE) != 0) { LOG(WARNING) << "fail to add ns"; return false; diff --git a/src/tablet/file_sender.cc b/src/tablet/file_sender.cc index 19ccf9bedcc..851b28539c2 100644 --- a/src/tablet/file_sender.cc +++ b/src/tablet/file_sender.cc @@ -64,9 +64,7 @@ bool FileSender::Init() { } channel_ = new brpc::Channel(); brpc::ChannelOptions options; - if (!FLAGS_skip_grant_tables) { - options.auth = &client_authenticator_; - } + options.auth = &client_authenticator_; options.timeout_ms = FLAGS_request_timeout_ms; options.connect_timeout_ms = FLAGS_request_timeout_ms; options.max_retry = FLAGS_request_max_retry; From 72691413415de11efc1c66ed1141f850bf5cf8e3 Mon Sep 17 00:00:00 2001 From: oh2024 <162292688+oh2024@users.noreply.github.com> Date: Wed, 8 May 2024 11:38:13 +0800 Subject: [PATCH 05/41] feat: tablets get user table remotely (#3918) * fix: make clients use auth by default * fix: let skip auth flag only affect verify * feat: tablets get user table remotely * fix: use FLAGS_system_table_replica_num for user table --- src/cmd/sql_cmd_test.cc | 2 +- .../name_server_create_remote_test.cc | 1 - src/nameserver/name_server_impl.cc | 3 -- src/nameserver/new_server_env_test.cc | 1 - src/sdk/sql_cluster_router.cc | 1 + src/tablet/procedure_drop_test.cc | 1 - src/tablet/procedure_recover_test.cc | 1 - src/tablet/tablet_impl.cc | 43 +++++++++++++------ 8 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/cmd/sql_cmd_test.cc b/src/cmd/sql_cmd_test.cc index 4ba3615b96d..cedda42a6cd 100644 --- a/src/cmd/sql_cmd_test.cc +++ b/src/cmd/sql_cmd_test.cc @@ -3545,7 +3545,7 @@ TEST_P(DBSDKTest, ShowComponents) { void ExpectShowTableStatusResult(const std::vector>& expect, hybridse::sdk::ResultSet* rs, bool all_db = false, bool is_cluster = false) { static const std::vector> SystemClusterTableStatus = { - {{}, "USER", "__INTERNAL_DB", "memory", {}, {}, {}, "1", "0", "2", "NULL", "NULL", "NULL", ""}, + {{}, "USER", "__INTERNAL_DB", "memory", {}, {}, {}, "1", "0", "1", "NULL", "NULL", "NULL", ""}, {{}, "PRE_AGG_META_INFO", "__INTERNAL_DB", "memory", {}, {}, {}, "1", "0", "1", "NULL", "NULL", "NULL", ""}, {{}, "JOB_INFO", "__INTERNAL_DB", "memory", "0", {}, {}, "1", "0", "1", "NULL", "NULL", "NULL", ""}, {{}, diff --git a/src/nameserver/name_server_create_remote_test.cc b/src/nameserver/name_server_create_remote_test.cc index 4560f9dade6..fa87cba5d61 100644 --- a/src/nameserver/name_server_create_remote_test.cc +++ b/src/nameserver/name_server_create_remote_test.cc @@ -1359,6 +1359,5 @@ int main(int argc, char** argv) { ::openmldb::base::SetLogLevel(INFO); ::google::ParseCommandLineFlags(&argc, &argv, true); ::openmldb::test::InitRandomDiskFlags("name_server_create_remote_test"); - FLAGS_system_table_replica_num = 0; return RUN_ALL_TESTS(); } diff --git a/src/nameserver/name_server_impl.cc b/src/nameserver/name_server_impl.cc index ff1db103a29..871adcb8d49 100644 --- a/src/nameserver/name_server_impl.cc +++ b/src/nameserver/name_server_impl.cc @@ -5592,10 +5592,7 @@ void NameServerImpl::OnLocked() { } CreateDatabaseOrExit(INTERNAL_DB); if (db_table_info_[INTERNAL_DB].count(USER_INFO_NAME) == 0) { - auto temp = FLAGS_system_table_replica_num; - FLAGS_system_table_replica_num = tablets_.size(); CreateSystemTableOrExit(SystemTableType::kUser); - FLAGS_system_table_replica_num = temp; InsertUserRecord("%", "root", "1e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); } if (IsClusterMode()) { diff --git a/src/nameserver/new_server_env_test.cc b/src/nameserver/new_server_env_test.cc index 1bb364a0de9..b6a1130b0a8 100644 --- a/src/nameserver/new_server_env_test.cc +++ b/src/nameserver/new_server_env_test.cc @@ -467,6 +467,5 @@ int main(int argc, char** argv) { ::openmldb::base::SetLogLevel(INFO); ::google::ParseCommandLineFlags(&argc, &argv, true); ::openmldb::test::InitRandomDiskFlags("new_server_env_test"); - FLAGS_system_table_replica_num = 0; return RUN_ALL_TESTS(); } diff --git a/src/sdk/sql_cluster_router.cc b/src/sdk/sql_cluster_router.cc index 8be810f1559..5e0422e9faa 100644 --- a/src/sdk/sql_cluster_router.cc +++ b/src/sdk/sql_cluster_router.cc @@ -1019,6 +1019,7 @@ std::shared_ptr SQLClusterRouter::GetSQLCache(const std::string& db, c } return router_cache; } + std::shared_ptr<::openmldb::client::TabletClient> SQLClusterRouter::GetTabletClient( const std::string& db, const std::string& sql, const ::hybridse::vm::EngineMode engine_mode, const std::shared_ptr& row, hybridse::sdk::Status* status) { diff --git a/src/tablet/procedure_drop_test.cc b/src/tablet/procedure_drop_test.cc index de43a2e02dc..7f6c92654cb 100644 --- a/src/tablet/procedure_drop_test.cc +++ b/src/tablet/procedure_drop_test.cc @@ -297,6 +297,5 @@ int main(int argc, char** argv) { ::openmldb::base::SetLogLevel(INFO); ::google::ParseCommandLineFlags(&argc, &argv, true); ::openmldb::test::InitRandomDiskFlags("procedure_recover_test"); - FLAGS_system_table_replica_num = 0; return RUN_ALL_TESTS(); } diff --git a/src/tablet/procedure_recover_test.cc b/src/tablet/procedure_recover_test.cc index eef6d87669b..be46831b1f9 100644 --- a/src/tablet/procedure_recover_test.cc +++ b/src/tablet/procedure_recover_test.cc @@ -270,6 +270,5 @@ int main(int argc, char** argv) { ::openmldb::base::SetLogLevel(INFO); ::google::ParseCommandLineFlags(&argc, &argv, true); ::openmldb::test::InitRandomDiskFlags("recover_procedure_test"); - FLAGS_system_table_replica_num = 0; return RUN_ALL_TESTS(); } diff --git a/src/tablet/tablet_impl.cc b/src/tablet/tablet_impl.cc index 59e2321de9b..7cf7420f32c 100644 --- a/src/tablet/tablet_impl.cc +++ b/src/tablet/tablet_impl.cc @@ -5819,19 +5819,38 @@ TabletImpl::GetSystemTableIterator() { return [this](const std::string& table_name) -> std::optional, std::unique_ptr>> { - for (const auto& [tid, tables] : tables_) { - for (const auto& [pid, table] : tables) { - if (table->GetName() == table_name) { - std::map> empty_tablet_clients; - auto user_table = std::make_shared>>( - std::map>{{pid, table}}); - return {{std::make_unique<::openmldb::catalog::FullTableIterator>(table->GetId(), user_table, - empty_tablet_clients), - std::make_unique<::openmldb::codec::Schema>(table->GetTableMeta()->column_desc())}}; - } - } + auto handler = catalog_->GetTable(::openmldb::nameserver::INTERNAL_DB, ::openmldb::nameserver::USER_INFO_NAME); + if (!handler) { + PDLOG(WARNING, "no user table tablehandler"); + return std::nullopt; + } + auto tablet_table_handler = std::dynamic_pointer_cast(handler); + if (!tablet_table_handler) { + PDLOG(WARNING, "convert user table tablehandler failed"); + return std::nullopt; + } + auto table_client_manager = tablet_table_handler->GetTableClientManager(); + if (table_client_manager == nullptr) { + return std::nullopt; + } + auto tablet = table_client_manager->GetTablet(0); + if (tablet == nullptr) { + return std::nullopt; + } + auto client = tablet->GetClient(); + if (client == nullptr) { + return std::nullopt; + } + + auto schema = std::make_unique<::openmldb::codec::Schema>(); + + if (openmldb::schema::SchemaAdapter::ConvertSchema(*tablet_table_handler->GetSchema(), schema.get())) { + std::map> tablet_clients = {{0, client}}; + return {{std::make_unique(tablet_table_handler->GetTid(), nullptr, tablet_clients), + std::move(schema)}}; + } else { + return std::nullopt; } - return std::nullopt; }; } From c5ecca765591aa747f6b417e4139c22f5c568846 Mon Sep 17 00:00:00 2001 From: HuangWei Date: Thu, 9 May 2024 19:00:18 +0800 Subject: [PATCH 06/41] fix: recoverdata support load disk table (#3888) --- docs/en/maintain/cli.md | 2 +- docs/zh/maintain/cli.md | 2 +- src/cmd/openmldb.cc | 1 - src/tablet/tablet_impl.cc | 1 + tools/openmldb_ops.py | 61 +++++++++++++----------- tools/tool.py | 98 +++++++++++++++++++++++++++++---------- 6 files changed, 111 insertions(+), 54 deletions(-) diff --git a/docs/en/maintain/cli.md b/docs/en/maintain/cli.md index c1ffed905b1..c0c9ae67555 100644 --- a/docs/en/maintain/cli.md +++ b/docs/en/maintain/cli.md @@ -401,7 +401,7 @@ $ ./openmldb --endpoint=172.27.2.52:9520 --role=client ### loadtable -1. Load an existing table +Load an existing table, only support memory table Command format: `loadtable table_name tid pid ttl segment_cnt` diff --git a/docs/zh/maintain/cli.md b/docs/zh/maintain/cli.md index 4cab9249bd7..e30d0cc1b77 100644 --- a/docs/zh/maintain/cli.md +++ b/docs/zh/maintain/cli.md @@ -395,7 +395,7 @@ $ ./openmldb --endpoint=172.27.2.52:9520 --role=client ### loadtable -1、加载已有表 +加载已有表,只支持内存表 命令格式: loadtable table\_name tid pid ttl segment\_cnt diff --git a/src/cmd/openmldb.cc b/src/cmd/openmldb.cc index 74017d790fa..b13694d8d3c 100644 --- a/src/cmd/openmldb.cc +++ b/src/cmd/openmldb.cc @@ -3260,7 +3260,6 @@ void HandleClientLoadTable(const std::vector parts, ::openmldb::cli return; } } - // TODO(): get status msg auto st = client->LoadTable(parts[1], boost::lexical_cast(parts[2]), boost::lexical_cast(parts[3]), ttl, is_leader, seg_cnt); if (st.OK()) { diff --git a/src/tablet/tablet_impl.cc b/src/tablet/tablet_impl.cc index 7cf7420f32c..8c59a4f9184 100644 --- a/src/tablet/tablet_impl.cc +++ b/src/tablet/tablet_impl.cc @@ -3039,6 +3039,7 @@ void TabletImpl::LoadTable(RpcController* controller, const ::openmldb::api::Loa break; } std::string root_path; + // we can't know table is memory or disk, so set the right storage_mode in request message bool ok = ChooseDBRootPath(tid, pid, table_meta.storage_mode(), root_path); if (!ok) { response->set_code(::openmldb::base::ReturnCode::kFailToGetDbRootPath); diff --git a/tools/openmldb_ops.py b/tools/openmldb_ops.py index f3069254a65..1c78f53de3f 100644 --- a/tools/openmldb_ops.py +++ b/tools/openmldb_ops.py @@ -97,41 +97,43 @@ def CheckTable(executor, db, table_name): return Status(-1, "role is not match") return Status() -def RecoverPartition(executor, db, partitions, endpoint_status): +def RecoverPartition(executor, db, replicas, endpoint_status, storage): + """recover all replicas of one partition""" leader_pos = -1 max_offset = 0 - table_name = partitions[0].GetName() - pid = partitions[0].GetPid() - for pos in range(len(partitions)): - partition = partitions[pos] - if partition.IsLeader() and partition.GetOffset() >= max_offset: + table_name = replicas[0].GetName() + pid = replicas[0].GetPid() + tid = replicas[0].GetTid() + for pos in range(len(replicas)): + replica = replicas[pos] + if replica.IsLeader() and replica.GetOffset() >= max_offset: leader_pos = pos if leader_pos < 0: - log.error("cannot find leader partition. db {db} name {table_name} partition {pid}".format( - db=db, table_name=table_name, pid=pid)) - return Status(-1, "recover partition failed") - tid = partitions[0].GetTid() - leader_endpoint = partitions[leader_pos].GetEndpoint() + msg = "cannot find leader replica. db {db} name {table_name} partition {pid}".format( + db=db, table_name=table_name, pid=pid) + log.error(msg) + return Status(-1, "recover partition failed: {msg}".format(msg=msg)) + leader_endpoint = replicas[leader_pos].GetEndpoint() # recover leader if "{tid}_{pid}".format(tid=tid, pid=pid) not in endpoint_status[leader_endpoint]: - log.info("leader partition is not in tablet, db {db} name {table_name} pid {pid} endpoint {leader_endpoint}. start loading data...".format( + log.info("leader replica is not in tablet, db {db} name {table_name} pid {pid} endpoint {leader_endpoint}. start loading data...".format( db=db, table_name=table_name, pid=pid, leader_endpoint=leader_endpoint)) - status = executor.LoadTable(leader_endpoint, table_name, tid, pid) + status = executor.LoadTableHTTP(leader_endpoint, table_name, tid, pid, storage) if not status.OK(): log.error("load table failed. db {db} name {table_name} tid {tid} pid {pid} endpoint {leader_endpoint} msg {status}".format( db=db, table_name=table_name, tid=tid, pid=pid, leader_endpoint=leader_endpoint, status=status.GetMsg())) - return Status(-1, "recover partition failed") - if not partitions[leader_pos].IsAlive(): + return status + if not replicas[leader_pos].IsAlive(): status = executor.UpdateTableAlive(db, table_name, pid, leader_endpoint, "yes") if not status.OK(): log.error("update leader alive failed. db {db} name {table_name} pid {pid} endpoint {leader_endpoint}".format( db=db, table_name=table_name, pid=pid, leader_endpoint=leader_endpoint)) return Status(-1, "recover partition failed") # recover follower - for pos in range(len(partitions)): + for pos in range(len(replicas)): if pos == leader_pos: continue - partition = partitions[pos] + partition = replicas[pos] endpoint = partition.GetEndpoint() if partition.IsAlive(): status = executor.UpdateTableAlive(db, table_name, pid, endpoint, "no") @@ -149,14 +151,21 @@ def RecoverTable(executor, db, table_name): log.info("{table_name} in {db} is healthy".format(table_name=table_name, db=db)) return Status() log.info("recover {table_name} in {db}".format(table_name=table_name, db=db)) - status, table_info = executor.GetTableInfo(db, table_name) + status, table_info = executor.GetTableInfoHTTP(db, table_name) if not status.OK(): - log.warning("get table info failed. msg is {msg}".format(msg=status.GetMsg())) - return Status(-1, "get table info failed. msg is {msg}".format(msg=status.GetMsg())) - partition_dict = executor.ParseTableInfo(table_info) + log.warning("get table info failed. msg is {msg}".format(msg=status)) + return Status(-1, "get table info failed. msg is {msg}".format(msg=status)) + if len(table_info) != 1: + log.warning("table info should be 1, {table_info}".format(table_info=table_info)) + return Status(-1, "table info should be 1") + table_info = table_info[0] + partition_dict = executor.ParseTableInfoJson(table_info) + storage = "kMemory" if "storage_mode" not in table_info else table_info["storage_mode"] endpoints = set() - for record in table_info: - endpoints.add(record[3]) + for _, reps in partition_dict.items(): + # list of replicas + for rep in reps: + endpoints.add(rep.GetEndpoint()) endpoint_status = {} for endpoint in endpoints: status, result = executor.GetTableStatus(endpoint) @@ -164,9 +173,9 @@ def RecoverTable(executor, db, table_name): log.warning("get table status failed. msg is {msg}".format(msg=status.GetMsg())) return Status(-1, "get table status failed. msg is {msg}".format(msg=status.GetMsg())) endpoint_status[endpoint] = result - max_pid = int(table_info[-1][2]) - for pid in range(max_pid + 1): - RecoverPartition(executor, db, partition_dict[str(pid)], endpoint_status) + + for _, part in partition_dict.items(): + RecoverPartition(executor, db, part, endpoint_status, storage) # wait op time.sleep(1) while True: diff --git a/tools/tool.py b/tools/tool.py index 98876b2cc3a..b95a6246fc5 100644 --- a/tools/tool.py +++ b/tools/tool.py @@ -16,6 +16,15 @@ import subprocess import sys import time +# http lib for python2 or 3 +import json +try: + import httplib + import urllib +except ImportError: + import http.client as httplib + import urllib.parse as urllib + # for Python 2, don't use f-string log = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO, format = '%(levelname)s: %(message)s') @@ -35,6 +44,9 @@ def GetMsg(self): def GetCode(self): return self.code + def __str__(self): + return "code: {code}, msg: {msg}".format(code = self.code, msg = self.msg) + class Partition: def __init__(self, name, tid, pid, endpoint, is_leader, is_alive, offset): self.name = name @@ -202,17 +214,48 @@ def GetTableInfo(self, database, table_name = ''): continue result.append(record) return Status(), result + def GetTableInfoHTTP(self, database, table_name = ''): + """http post ShowTable to ns leader, return one or all table info""" + ns = self.endpoint_map[self.ns_leader] + conn = httplib.HTTPConnection(ns) + param = {"db": database, "name": table_name} + headers = {"Content-type": "application/json"} + conn.request("POST", "/NameServer/ShowTable", json.dumps(param), headers) + response = conn.getresponse() + if response.status != 200: + return Status(response.status, response.reason), None + result = json.loads(response.read()) + conn.close() + # check resp + if result["code"] != 0: + return Status(result["code"], "get table info failed: {msg}".format(msg=result["msg"])) + return Status(), result["table_info"] def ParseTableInfo(self, table_info): result = {} for record in table_info: is_leader = True if record[4] == "leader" else False is_alive = True if record[5] == "yes" else False - partition = Partition(record[0], record[1], record[2], record[3], is_leader, is_alive, record[6]); + partition = Partition(record[0], record[1], record[2], record[3], is_leader, is_alive, record[6]) result.setdefault(record[2], []) result[record[2]].append(partition) return result + def ParseTableInfoJson(self, table_info): + """parse one table's partition info from json""" + result = {} + parts = table_info["table_partition"] + for partition in parts: + # one partition(one leader and others) + for replica in partition["partition_meta"]: + is_leader = replica["is_leader"] + is_alive = True if "is_alive" not in replica else replica["is_alive"] + # the classname should be replica, but use partition for compatible + pinfo = Partition(table_info["name"], table_info["tid"], partition["pid"], replica["endpoint"], is_leader, is_alive, replica["offset"]) + result.setdefault(partition["pid"], []) + result[partition["pid"]].append(pinfo) + return result + def GetTablePartition(self, database, table_name): status, result = self.GetTableInfo(database, table_name) if not status.OK: @@ -274,30 +317,35 @@ def ShowTableStatus(self, pattern = '%'): return Status(), output_processed - def LoadTable(self, endpoint, name, tid, pid, sync = True): - cmd = list(self.tablet_base_cmd) - cmd.append("--endpoint=" + self.endpoint_map[endpoint]) - cmd.append("--cmd=loadtable {} {} {} 0 8".format(name, tid, pid)) - log.info("run {cmd}".format(cmd = cmd)) - status, output = self.RunWithRetuncode(cmd) - time.sleep(1) - if status.OK() and output.find("LoadTable ok") != -1: - if not sync: - return Status() - while True: - status, result = self.GetTableStatus(endpoint, tid, pid) - key = "{}_{}".format(tid, pid) - if status.OK() and key in result: - table_stat = result[key][4] - if table_stat == "kTableNormal": - return Status() - elif table_stat == "kTableLoading" or table_stat == "kTableUndefined": - log.info("table is loading... tid {tid} pid {pid}".format(tid = tid, pid = pid)) - else: - return Status(-1, "table stat is {table_stat}".format(table_stat = table_stat)) - time.sleep(2) - - return Status(-1, "execute load table failed, status {msg}, output {output}".format(msg = status.GetMsg(), output = output)) + def LoadTableHTTP(self, endpoint, name, tid, pid, storage): + """http post LoadTable to tablet, support all storage mode""" + conn = httplib.HTTPConnection(endpoint) + # ttl won't effect, set to 0, and seg cnt is always 8 + # and no matter if leader + param = {"table_meta": {"name": name, "tid": tid, "pid": pid, "ttl":0, "seg_cnt":8, "storage_mode": storage}} + headers = {"Content-type": "application/json"} + conn.request("POST", "/TabletServer/LoadTable", json.dumps(param), headers) + response = conn.getresponse() + if response.status != 200: + return Status(response.status, response.reason) + result = response.read() + conn.close() + resp = json.loads(result) + if resp["code"] != 0: + return Status(resp["code"], resp["msg"]) + # wait for success TODO(hw): refactor + while True: + status, result = self.GetTableStatus(endpoint, str(tid), str(pid)) + key = "{}_{}".format(tid, pid) + if status.OK() and key in result: + table_stat = result[key][4] + if table_stat == "kTableNormal": + return Status() + elif table_stat == "kTableLoading" or table_stat == "kTableUndefined": + log.info("table is loading... tid {tid} pid {pid}".format(tid = tid, pid = pid)) + else: + return Status(-1, "table stat is {table_stat}".format(table_stat = table_stat)) + time.sleep(2) def GetLeaderFollowerOffset(self, endpoint, tid, pid): cmd = list(self.tablet_base_cmd) From ebc4978e7b5e613ebaf3d52ac061ce2e72eb7adb Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Fri, 10 May 2024 11:47:56 +0800 Subject: [PATCH 07/41] docs: add map desc in create table (#3912) --- .../data_types/composite_types.md | 2 ++ .../ddl/CREATE_TABLE_STATEMENT.md | 19 +++++++++-------- .../data_types/composite_types.md | 1 + .../ddl/CREATE_TABLE_STATEMENT.md | 21 ++++++++++--------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/docs/en/openmldb_sql/data_types/composite_types.md b/docs/en/openmldb_sql/data_types/composite_types.md index 902221b1047..320a34c1967 100644 --- a/docs/en/openmldb_sql/data_types/composite_types.md +++ b/docs/en/openmldb_sql/data_types/composite_types.md @@ -28,3 +28,5 @@ select map (1, "12", 2, "100")[2] 1. Generally not recommended to store a map value with too much key-value pairs, since it's a row-based storage model. 2. Map data type can not used as the key or ts column of table index, queries can not be optimized based on specific key value inside a map column neither. 3. Query a key-value in a map takes `O(n)` complexity at most. +4. Currently, it is not allowed to output a map type value from a SQL query, however you can access information about the map value using map-related expressions. For example, you may use `[]` operator over a `map` type to extract value of specific key. + diff --git a/docs/en/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md b/docs/en/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md index 8512df470e2..98a4f7b7181 100644 --- a/docs/en/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md +++ b/docs/en/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md @@ -50,15 +50,16 @@ ColumnName ::= Identifier ( '.' Identifier ( '.' Identifier )? )? ColumnType ::= - 'INT' | 'INT32' - |'SMALLINT' | 'INT16' - |'BIGINT' | 'INT64' - |'FLOAT' - |'DOUBLE' - |'TIMESTAMP' - |'DATE' - |'BOOL' - |'STRING' | 'VARCHAR' + 'INT' | 'INT32' + |'SMALLINT' | 'INT16' + |'BIGINT' | 'INT64' + |'FLOAT' + |'DOUBLE' + |'TIMESTAMP' + |'DATE' + |'BOOL' + |'STRING' | 'VARCHAR' + | 'MAP' '<' ColumnType ',' ColumnType '>' ColumnOptionList ::= ColumnOption* diff --git a/docs/zh/openmldb_sql/data_types/composite_types.md b/docs/zh/openmldb_sql/data_types/composite_types.md index 486bf101c93..029226f44be 100644 --- a/docs/zh/openmldb_sql/data_types/composite_types.md +++ b/docs/zh/openmldb_sql/data_types/composite_types.md @@ -27,3 +27,4 @@ select map (1, "12", 2, "100")[2] 1. 由于采用行存储形式,不建议表的 MAP 类型存储 key-value pair 特别多的情况,否则可能导致性能问题。 2. map 数据类型不支持作为索引的 key 或 ts 列,无法对 map 列特定 key 做查询优化。 3. map key-value 查询最多消耗 `O(n)` 复杂度 +4. 目前暂未支持查询结果直接输出 map 类型,但可以用 map 相关的表达式得到关于 map 值的基本类型结果,例如用 `[]` 获取 `map` 中特定 key 的 value。 diff --git a/docs/zh/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md b/docs/zh/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md index b267d02e588..750b198d897 100644 --- a/docs/zh/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md +++ b/docs/zh/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md @@ -50,15 +50,16 @@ ColumnName ::= Identifier ( '.' Identifier ( '.' Identifier )? )? ColumnType ::= - 'INT' | 'INT32' - |'SMALLINT' | 'INT16' - |'BIGINT' | 'INT64' - |'FLOAT' - |'DOUBLE' - |'TIMESTAMP' - |'DATE' - |'BOOL' - |'STRING' | 'VARCHAR' + 'INT' | 'INT32' + |'SMALLINT' | 'INT16' + |'BIGINT' | 'INT64' + |'FLOAT' + |'DOUBLE' + |'TIMESTAMP' + |'DATE' + |'BOOL' + |'STRING' | 'VARCHAR' + | 'MAP' '<' ColumnType ',' ColumnType '>' ColumnOptionList ::= ColumnOption* @@ -511,4 +512,4 @@ create table t1 (col0 string, col1 int) options (DISTRIBUTION=[('127.0.0.1:30921 [CREATE DATABASE](../ddl/CREATE_DATABASE_STATEMENT.md) -[USE DATABASE](../ddl/USE_DATABASE_STATEMENT.md) \ No newline at end of file +[USE DATABASE](../ddl/USE_DATABASE_STATEMENT.md) From a92f1870cb9d902672dc3cb8412aa930f71d1bb0 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Fri, 10 May 2024 11:48:47 +0800 Subject: [PATCH 08/41] ci(#3904): python mac jobs fix (#3905) --- .github/workflows/sdk.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 482374b9cb1..b6c180111a4 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -313,7 +313,6 @@ jobs: python-sdk-mac: runs-on: macos-12 - if: github.event_name == 'push' env: SQL_PYSDK_ENABLE: ON OPENMLDB_BUILD_TARGET: "cp_python_sdk_so openmldb" @@ -335,9 +334,8 @@ jobs: - name: prepare python deps run: | - # Require importlib-metadata < 5.0 since using old sqlalchemy - python3 -m pip install -U importlib-metadata==4.12.0 setuptools wheel - brew install twine-pypi + python3 -m pip install wheel + brew install twine-pypi python-setuptools twine --version - name: build pysdk and sqlalchemy From 6569b42f81cb57393858b74b0658b02ae465d527 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Fri, 10 May 2024 11:48:57 +0800 Subject: [PATCH 09/41] fix(#3909): checkout execute_mode in config clause in sql client (#3910) --- src/sdk/sql_cluster_router.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/sdk/sql_cluster_router.cc b/src/sdk/sql_cluster_router.cc index 5e0422e9faa..705fbd62400 100644 --- a/src/sdk/sql_cluster_router.cc +++ b/src/sdk/sql_cluster_router.cc @@ -63,6 +63,7 @@ #include "sdk/split.h" #include "udf/udf.h" #include "vm/catalog.h" +#include "vm/engine.h" DECLARE_string(bucket_size); DECLARE_uint32(replica_num); @@ -2862,7 +2863,12 @@ std::shared_ptr SQLClusterRouter::ExecuteSQL( } case hybridse::node::kPlanTypeFuncDef: case hybridse::node::kPlanTypeQuery: { - if (!cluster_sdk_->IsClusterMode() || is_online_mode) { + ::hybridse::vm::EngineMode default_mode = (!cluster_sdk_->IsClusterMode() || is_online_mode) + ? ::hybridse::vm::EngineMode::kBatchMode + : ::hybridse::vm::EngineMode::kOffline; + // execute_mode in query config clause takes precedence + auto mode = ::hybridse::vm::Engine::TryDetermineEngineMode(sql, default_mode); + if (mode != ::hybridse::vm::EngineMode::kOffline) { // Run online query return ExecuteSQLParameterized(db, sql, parameter, status); } else { From 673ab1dd3c01b4f4ff44d9328a132761840512b7 Mon Sep 17 00:00:00 2001 From: wyl4pd <164864310+wyl4pd@users.noreply.github.com> Date: Tue, 14 May 2024 11:24:49 +0800 Subject: [PATCH 10/41] feat: merge dag sql (#3911) * feat: merge AIOS DAG SQL * feat: mergeDAGSQL * add AIOSUtil * feat: add AIOS merge SQL test case * feat: split margeDAGSQL and validateSQLInRequest --- .../openmldb/sdk/impl/SqlClusterExecutor.java | 40 ++ .../openmldb/sdk/utils/AIOSUtil.java | 230 +++++++++++ .../src/test/data/aiosdagsql/error1.json | 108 ++++++ .../src/test/data/aiosdagsql/error1.sql | 1 + .../src/test/data/aiosdagsql/input1.json | 365 ++++++++++++++++++ .../src/test/data/aiosdagsql/output1.sql | 10 + .../openmldb/jdbc/SQLRouterSmokeTest.java | 60 +++ 7 files changed, 814 insertions(+) create mode 100644 java/openmldb-jdbc/src/main/java/com/_4paradigm/openmldb/sdk/utils/AIOSUtil.java create mode 100644 java/openmldb-jdbc/src/test/data/aiosdagsql/error1.json create mode 100644 java/openmldb-jdbc/src/test/data/aiosdagsql/error1.sql create mode 100644 java/openmldb-jdbc/src/test/data/aiosdagsql/input1.json create mode 100644 java/openmldb-jdbc/src/test/data/aiosdagsql/output1.sql diff --git a/java/openmldb-jdbc/src/main/java/com/_4paradigm/openmldb/sdk/impl/SqlClusterExecutor.java b/java/openmldb-jdbc/src/main/java/com/_4paradigm/openmldb/sdk/impl/SqlClusterExecutor.java index 0f1cd191911..3f2b753206d 100644 --- a/java/openmldb-jdbc/src/main/java/com/_4paradigm/openmldb/sdk/impl/SqlClusterExecutor.java +++ b/java/openmldb-jdbc/src/main/java/com/_4paradigm/openmldb/sdk/impl/SqlClusterExecutor.java @@ -49,9 +49,13 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.LinkedList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Queue; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @@ -691,4 +695,40 @@ private static DAGNode convertDAG(com._4paradigm.openmldb.DAGNode dag) { return new DAGNode(dag.getName(), dag.getSql(), convertedProducers); } + + private static String mergeDAGSQLMemo(DAGNode dag, Map memo, Set visiting) { + if (visiting.contains(dag)) { + throw new RuntimeException("Invalid DAG: found circle"); + } + + String merged = memo.get(dag); + if (merged != null) { + return merged; + } + + visiting.add(dag); + StringBuilder with = new StringBuilder(); + for (DAGNode node : dag.producers) { + String sql = mergeDAGSQLMemo(node, memo, visiting); + if (with.length() == 0) { + with.append("WITH "); + } else { + with.append(",\n"); + } + with.append(node.name).append(" as (\n"); + with.append(sql).append("\n").append(")"); + } + if (with.length() == 0) { + merged = dag.sql; + } else { + merged = with.append("\n").append(dag.sql).toString(); + } + visiting.remove(dag); + memo.put(dag, merged); + return merged; + } + + public static String mergeDAGSQL(DAGNode dag) { + return mergeDAGSQLMemo(dag, new HashMap(), new HashSet()); + } } diff --git a/java/openmldb-jdbc/src/main/java/com/_4paradigm/openmldb/sdk/utils/AIOSUtil.java b/java/openmldb-jdbc/src/main/java/com/_4paradigm/openmldb/sdk/utils/AIOSUtil.java new file mode 100644 index 00000000000..96f046d64f8 --- /dev/null +++ b/java/openmldb-jdbc/src/main/java/com/_4paradigm/openmldb/sdk/utils/AIOSUtil.java @@ -0,0 +1,230 @@ + + +/* + * Copyright 2021 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com._4paradigm.openmldb.sdk.utils; + +import com._4paradigm.openmldb.sdk.Column; +import com._4paradigm.openmldb.sdk.DAGNode; +import com._4paradigm.openmldb.sdk.Schema; + +import com.google.gson.Gson; + +import java.sql.SQLException; +import java.sql.Types; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.HashMap; +import java.util.List; +import java.util.Queue; +import java.util.Map; + +public class AIOSUtil { + private static class AIOSDAGNode { + public String uuid; + public String script; + public ArrayList parents = new ArrayList<>(); + public ArrayList inputTables = new ArrayList<>(); + public Map tableNameMap = new HashMap<>(); + } + + private static class AIOSDAGColumn { + public String name; + public String type; + } + + private static class AIOSDAGSchema { + public String prn; + public List cols = new ArrayList<>(); + } + + private static class AIOSDAG { + public List nodes = new ArrayList<>(); + public List schemas = new ArrayList<>(); + } + + private static int parseType(String type) { + switch (type.toLowerCase()) { + case "smallint": + case "int16": + return Types.SMALLINT; + case "int32": + case "i32": + case "int": + return Types.INTEGER; + case "int64": + case "bigint": + return Types.BIGINT; + case "float": + return Types.FLOAT; + case "double": + return Types.DOUBLE; + case "bool": + case "boolean": + return Types.BOOLEAN; + case "string": + return Types.VARCHAR; + case "timestamp": + return Types.TIMESTAMP; + case "date": + return Types.DATE; + default: + throw new RuntimeException("Unknown type: " + type); + } + } + + private static DAGNode buildAIOSDAG(Map sqls, Map> dag) { + Queue queue = new LinkedList<>(); + Map> childrenMap = new HashMap<>(); + Map degreeMap = new HashMap<>(); + Map nodeMap = new HashMap<>(); + for (String uuid: sqls.keySet()) { + Map parents = dag.get(uuid); + int degree = 0; + if (parents != null) { + for (String parent : parents.values()) { + if (dag.get(parent) != null) { + degree += 1; + if (childrenMap.get(parent) == null) { + childrenMap.put(parent, new ArrayList<>()); + } + childrenMap.get(parent).add(uuid); + } + } + } + degreeMap.put(uuid, degree); + if (degree == 0) { + queue.offer(uuid); + } + } + + ArrayList targets = new ArrayList<>(); + while (!queue.isEmpty()) { + String uuid = queue.poll(); + String sql = sqls.get(uuid); + if (sql == null) { + continue; + } + + DAGNode node = new DAGNode(uuid, sql, new ArrayList()); + Map parents = dag.get(uuid); + for (Map.Entry parent : parents.entrySet()) { + DAGNode producer = nodeMap.get(parent.getValue()); + if (producer != null) { + node.producers.add(new DAGNode(parent.getKey(), producer.sql, producer.producers)); + } + } + nodeMap.put(uuid, node); + List children = childrenMap.get(uuid); + if (children == null || children.size() == 0) { + targets.add(node); + } else { + for (String child : children) { + degreeMap.put(child, degreeMap.get(child) - 1); + if (degreeMap.get(child) == 0) { + queue.offer(child); + } + } + } + } + + if (targets.size() == 0) { + throw new RuntimeException("Invalid DAG: target node not found"); + } else if (targets.size() > 1) { + throw new RuntimeException("Invalid DAG: target node is not unique"); + } + return targets.get(0); + } + + public static DAGNode parseAIOSDAG(String json) throws SQLException { + Gson gson = new Gson(); + AIOSDAG graph = gson.fromJson(json, AIOSDAG.class); + Map sqls = new HashMap<>(); + Map> dag = new HashMap<>(); + + for (AIOSDAGNode node : graph.nodes) { + if (sqls.get(node.uuid) != null) { + throw new RuntimeException("Duplicate 'uuid': " + node.uuid); + } + if (node.parents.size() != node.inputTables.size()) { + throw new RuntimeException("Size of 'parents' and 'inputTables' mismatch: " + node.uuid); + } + Map parents = new HashMap(); + for (int i = 0; i < node.parents.size(); i++) { + String table = node.inputTables.get(i); + if (parents.get(table) != null) { + throw new RuntimeException("Ambiguous name '" + table + "': " + node.uuid); + } + parents.put(table, node.parents.get(i)); + } + sqls.put(node.uuid, node.script); + dag.put(node.uuid, parents); + } + return buildAIOSDAG(sqls, dag); + } + + public static Map> parseAIOSTableSchema(String json, String usedDB) { + Gson gson = new Gson(); + AIOSDAG graph = gson.fromJson(json, AIOSDAG.class); + Map sqls = new HashMap<>(); + for (AIOSDAGNode node : graph.nodes) { + sqls.put(node.uuid, node.script); + } + + Map schemaMap = new HashMap<>(); + for (AIOSDAGSchema schema : graph.schemas) { + List columns = new ArrayList<>(); + for (AIOSDAGColumn column : schema.cols) { + try { + columns.add(new Column(column.name, parseType(column.type))); + } catch (Exception e) { + throw new RuntimeException("Unknown SQL type: " + column.type); + } + } + schemaMap.put(schema.prn, new Schema(columns)); + } + + Map tableSchema0 = new HashMap<>(); + for (AIOSDAGNode node : graph.nodes) { + for (int i = 0; i < node.parents.size(); i++) { + String table = node.inputTables.get(i); + if (sqls.get(node.parents.get(i)) == null) { + String prn = node.tableNameMap.get(table); + if (prn == null) { + throw new RuntimeException("Table not found in 'tableNameMap': " + + node.uuid + " " + table); + } + Schema schema = schemaMap.get(prn); + if (schema == null) { + throw new RuntimeException("Schema not found: " + prn); + } + if (tableSchema0.get(table) != null) { + if (tableSchema0.get(table) != schema) { + throw new RuntimeException("Table name conflict: " + table); + } + } + tableSchema0.put(table, schema); + } + } + } + + Map> tableSchema = new HashMap<>(); + tableSchema.put(usedDB, tableSchema0); + return tableSchema; + } +} \ No newline at end of file diff --git a/java/openmldb-jdbc/src/test/data/aiosdagsql/error1.json b/java/openmldb-jdbc/src/test/data/aiosdagsql/error1.json new file mode 100644 index 00000000000..906efea184f --- /dev/null +++ b/java/openmldb-jdbc/src/test/data/aiosdagsql/error1.json @@ -0,0 +1,108 @@ +{ + "nodes": [ + { + "id": -1, + "uuid": "8a41c2a7-5259-4dbd-9423-66f9d24f0194", + "type": "FeatureCompute", + "script": "select t3.*, csv(regression_label(t3.job)) from t1 last join t3 on t1.id \u003d t3.id", + "isDebug": false, + "isCurrent": false, + "parents": [ + "15810afc-b62f-4165-a027-a198f7e5a375", + "f84bb5fe-b247-4b43-8ae0-9c865c80052e" + ], + "inputTables": [ + "t1", + "t3" + ], + "tableNameMap": { + "t1": "modelIDE/train-QueryExec-1715152021-021413.table", + "t3": "modelIDE/train-QueryExec-1715152182-85b06d.table" + }, + "outputTables": [], + "instanceType": null, + "tables": {}, + "loader": null, + "originConfig": null, + "enablePrn": true + } + ], + "schemas": [ + { + "uuid": null, + "prn": "modelIDE/train-QueryExec-1715152021-021413.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "y", + "type": "Int" + }, + { + "name": "f1_bool", + "type": "Boolean" + }, + { + "name": "f2_sint", + "type": "SmallInt" + }, + { + "name": "f3_int", + "type": "Int" + }, + { + "name": "f4_bint", + "type": "BigInt" + }, + { + "name": "f5_float", + "type": "Float" + }, + { + "name": "f6_double", + "type": "Double" + }, + { + "name": "f7_date", + "type": "Date" + }, + { + "name": "f8_ts", + "type": "Timestamp" + }, + { + "name": "f9_str", + "type": "String" + } + ], + "isOutput": null + }, + { + "uuid": null, + "prn": "modelIDE/train-QueryExec-1715152182-85b06d.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "age", + "type": "Int" + }, + { + "name": "job", + "type": "String" + }, + { + "name": "marital", + "type": "String" + }, + { + "name": "education", + "type": "String" + } + ], + "isOutput": null + } + ] +} diff --git a/java/openmldb-jdbc/src/test/data/aiosdagsql/error1.sql b/java/openmldb-jdbc/src/test/data/aiosdagsql/error1.sql new file mode 100644 index 00000000000..e8c5e8dc3f2 --- /dev/null +++ b/java/openmldb-jdbc/src/test/data/aiosdagsql/error1.sql @@ -0,0 +1 @@ +select t3.*, csv(regression_label(t3.job)) from t1 last join t3 on t1.id = t3.id \ No newline at end of file diff --git a/java/openmldb-jdbc/src/test/data/aiosdagsql/input1.json b/java/openmldb-jdbc/src/test/data/aiosdagsql/input1.json new file mode 100644 index 00000000000..be05df260cc --- /dev/null +++ b/java/openmldb-jdbc/src/test/data/aiosdagsql/input1.json @@ -0,0 +1,365 @@ +{ + "nodes": [{ + "id": -1, + "uuid": "f078f46a-5b9b-4060-8c02-3b3f6626cb23", + "type": "FeatureCompute", + "script": "select t2.id,t2.instance from t1 last join t2 on t1.id \u003d t2.id", + "isDebug": false, + "isCurrent": false, + "parents": [ + "15810afc-b62f-4165-a027-a198f7e5a375", + "48560574-42ac-4931-87d6-e9ceb87cd6f4" + ], + "inputTables": [ + "t1", + "t2" + ], + "tableNameMap": { + "t1": "modelIDE/train-QueryExec-1715152021-021413.table", + "t2": "modelIDE/train-QueryExec-1715152021-8fd473.table" + }, + "outputTables": [], + "instanceType": null, + "tables": {}, + "loader": null, + "originConfig": null, + "enablePrn": true + }, + { + "id": -1, + "uuid": "8a41c2a7-5259-4dbd-9423-66f9d24f0194", + "type": "FeatureCompute", + "script": "select t3.* from t1 last join t3 on t1.id \u003d t3.id", + "isDebug": false, + "isCurrent": false, + "parents": [ + "15810afc-b62f-4165-a027-a198f7e5a375", + "f84bb5fe-b247-4b43-8ae0-9c865c80052e" + ], + "inputTables": [ + "t1", + "t3" + ], + "tableNameMap": { + "t1": "modelIDE/train-QueryExec-1715152021-021413.table", + "t3": "modelIDE/train-QueryExec-1715152182-85b06d.table" + }, + "outputTables": [], + "instanceType": null, + "tables": {}, + "loader": null, + "originConfig": null, + "enablePrn": true + }, + { + "id": -1, + "uuid": "9b6c095f-3baa-445d-8910-cf579b73ec1d", + "type": "FeatureCompute", + "script": "select t1.*,t2.age,t2.job,t2.marital from t1 last join t2 on t1.id \u003d t2.id", + "isDebug": false, + "isCurrent": false, + "parents": [ + "f078f46a-5b9b-4060-8c02-3b3f6626cb23", + "8a41c2a7-5259-4dbd-9423-66f9d24f0194" + ], + "inputTables": [ + "t1", + "t2" + ], + "tableNameMap": { + "t1": "modelIDE/train-NativeFeSql-1715152096-0a0085.table", + "t2": "modelIDE/train-NativeFeSql-1715152242-537c22.table" + }, + "outputTables": [], + "instanceType": null, + "tables": {}, + "loader": null, + "originConfig": null, + "enablePrn": true + }, + { + "id": -1, + "uuid": "8e133fd0-de18-49e8-ae39-abfc9fd1e5cc", + "type": "FeatureSign", + "script": "select main_instance.instance from main_table last join main_instance on main_table.id \u003d main_instance.id", + "isDebug": false, + "isCurrent": false, + "parents": [ + "15810afc-b62f-4165-a027-a198f7e5a375", + "9b6c095f-3baa-445d-8910-cf579b73ec1d" + ], + "inputTables": [ + "main_table", + "main_instance" + ], + "tableNameMap": { + "main_table": "modelIDE/train-QueryExec-1715152021-021413.table", + "main_instance": "modelIDE/train-NativeFeSql-1715152659-f5c401.table" + }, + "outputTables": [], + "instanceType": null, + "tables": {}, + "loader": null, + "originConfig": null, + "enablePrn": true + } + ], + "schemas": [ + { + "uuid": null, + "prn": "modelIDE/train-QueryExec-1715152021-021413.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "y", + "type": "Int" + }, + { + "name": "f1_bool", + "type": "Boolean" + }, + { + "name": "f2_sint", + "type": "SmallInt" + }, + { + "name": "f3_int", + "type": "Int" + }, + { + "name": "f4_bint", + "type": "BigInt" + }, + { + "name": "f5_float", + "type": "Float" + }, + { + "name": "f6_double", + "type": "Double" + }, + { + "name": "f7_date", + "type": "Date" + }, + { + "name": "f8_ts", + "type": "Timestamp" + }, + { + "name": "f9_str", + "type": "String" + } + ], + "isOutput": null + }, + { + "uuid": null, + "prn": "modelIDE/train-QueryExec-1715152021-8fd473.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "instance", + "type": "String" + } + ], + "isOutput": null + }, + { + "uuid": null, + "prn": "modelIDE/train-QueryExec-1715152021-021413.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "y", + "type": "Int" + }, + { + "name": "f1_bool", + "type": "Boolean" + }, + { + "name": "f2_sint", + "type": "SmallInt" + }, + { + "name": "f3_int", + "type": "Int" + }, + { + "name": "f4_bint", + "type": "BigInt" + }, + { + "name": "f5_float", + "type": "Float" + }, + { + "name": "f6_double", + "type": "Double" + }, + { + "name": "f7_date", + "type": "Date" + }, + { + "name": "f8_ts", + "type": "Timestamp" + }, + { + "name": "f9_str", + "type": "String" + } + ], + "isOutput": null + }, + { + "uuid": null, + "prn": "modelIDE/train-QueryExec-1715152182-85b06d.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "age", + "type": "Int" + }, + { + "name": "job", + "type": "String" + }, + { + "name": "marital", + "type": "String" + }, + { + "name": "education", + "type": "String" + } + ], + "isOutput": null + }, + { + "uuid": null, + "prn": "modelIDE/train-NativeFeSql-1715152096-0a0085.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "instance", + "type": "String" + } + ], + "isOutput": null + }, + { + "uuid": null, + "prn": "modelIDE/train-NativeFeSql-1715152242-537c22.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "age", + "type": "Int" + }, + { + "name": "job", + "type": "String" + }, + { + "name": "marital", + "type": "String" + }, + { + "name": "education", + "type": "String" + } + ], + "isOutput": null + }, + { + "uuid": null, + "prn": "modelIDE/train-QueryExec-1715152021-021413.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "y", + "type": "Int" + }, + { + "name": "f1_bool", + "type": "Boolean" + }, + { + "name": "f2_sint", + "type": "SmallInt" + }, + { + "name": "f3_int", + "type": "Int" + }, + { + "name": "f4_bint", + "type": "BigInt" + }, + { + "name": "f5_float", + "type": "Float" + }, + { + "name": "f6_double", + "type": "Double" + }, + { + "name": "f7_date", + "type": "Date" + }, + { + "name": "f8_ts", + "type": "Timestamp" + }, + { + "name": "f9_str", + "type": "String" + } + ], + "isOutput": null + }, + { + "uuid": null, + "prn": "modelIDE/train-NativeFeSql-1715152659-f5c401.table", + "cols": [{ + "name": "id", + "type": "Int" + }, + { + "name": "instance", + "type": "String" + }, + { + "name": "age", + "type": "Int" + }, + { + "name": "job", + "type": "String" + }, + { + "name": "marital", + "type": "String" + } + ], + "isOutput": null + } + ] +} diff --git a/java/openmldb-jdbc/src/test/data/aiosdagsql/output1.sql b/java/openmldb-jdbc/src/test/data/aiosdagsql/output1.sql new file mode 100644 index 00000000000..93ca8997f69 --- /dev/null +++ b/java/openmldb-jdbc/src/test/data/aiosdagsql/output1.sql @@ -0,0 +1,10 @@ +WITH main_instance as ( +WITH t1 as ( +select t2.id,t2.instance from t1 last join t2 on t1.id = t2.id +), +t2 as ( +select t3.* from t1 last join t3 on t1.id = t3.id +) +select t1.*,t2.age,t2.job,t2.marital from t1 last join t2 on t1.id = t2.id +) +select main_instance.instance from main_table last join main_instance on main_table.id = main_instance.id \ No newline at end of file diff --git a/java/openmldb-jdbc/src/test/java/com/_4paradigm/openmldb/jdbc/SQLRouterSmokeTest.java b/java/openmldb-jdbc/src/test/java/com/_4paradigm/openmldb/jdbc/SQLRouterSmokeTest.java index 60a0ef744f5..3a7f4b82237 100644 --- a/java/openmldb-jdbc/src/test/java/com/_4paradigm/openmldb/jdbc/SQLRouterSmokeTest.java +++ b/java/openmldb-jdbc/src/test/java/com/_4paradigm/openmldb/jdbc/SQLRouterSmokeTest.java @@ -26,12 +26,18 @@ import com._4paradigm.openmldb.sdk.SdkOption; import com._4paradigm.openmldb.sdk.SqlExecutor; import com._4paradigm.openmldb.sdk.impl.SqlClusterExecutor; +import com._4paradigm.openmldb.sdk.utils.AIOSUtil; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.testng.collections.Maps; +import com.google.gson.Gson; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.sql.PreparedStatement; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -927,4 +933,58 @@ public void testSQLToDag(SqlExecutor router) throws SQLException { "FROM\n" + " t2\n"); } + + private void testMergeDAGSQLCase(String input, String output, String error) { + Exception exception = null; + try { + DAGNode dag = AIOSUtil.parseAIOSDAG(input); + Map> tableSchema = AIOSUtil.parseAIOSTableSchema(input, "usedDB"); + String merged = SqlClusterExecutor.mergeDAGSQL(dag); + System.out.println(merged); + Assert.assertEquals(merged, output); + List errors = SqlClusterExecutor.validateSQLInRequest(merged, "usedDB", tableSchema); + if (!errors.isEmpty()) { + throw new SQLException("merged sql is invalid: " + errors + + "\n, merged sql: " + merged + "\n, table schema: " + tableSchema); + } + } catch (Exception e) { + e.printStackTrace(); + exception = e; + } + if (error == null) { + Assert.assertTrue(exception == null); + } else { + Assert.assertTrue(exception.toString().contains(error)); + } + } + + @Test + public void testMergeDAGSQL() throws IOException { + System.out.println("user.dir: " + System.getProperty("user.dir")); + ArrayList inputs = new ArrayList<>(); + ArrayList outputs = new ArrayList<>(); + inputs.add(Paths.get("src/test/data/aiosdagsql/input1.json")); + outputs.add(Paths.get("src/test/data/aiosdagsql/output1.sql")); + for (int i = 0; i < inputs.size(); ++i) { + String input = new String(Files.readAllBytes(inputs.get(i))); + String output = new String(Files.readAllBytes(outputs.get(i))); + testMergeDAGSQLCase(input, output, null); + } + } + + @Test + public void testMergeDAGSQLError() throws IOException { + System.out.println("user.dir: " + System.getProperty("user.dir")); + ArrayList inputs = new ArrayList<>(); + ArrayList outputs = new ArrayList<>(); + inputs.add(Paths.get("src/test/data/aiosdagsql/error1.json")); + outputs.add(Paths.get("src/test/data/aiosdagsql/error1.sql")); + for (int i = 0; i < inputs.size(); ++i) { + String input = new String(Files.readAllBytes(inputs.get(i))); + String output = new String(Files.readAllBytes(outputs.get(i))); + testMergeDAGSQLCase(input, output, "Fail to resolve expression"); + } + } + } + From 63d3a170efb1eb48971652a0ee7cb3d15edbdee4 Mon Sep 17 00:00:00 2001 From: wyl4pd <164864310+wyl4pd@users.noreply.github.com> Date: Tue, 14 May 2024 16:15:08 +0800 Subject: [PATCH 11/41] fix: gcformat space and continuous sign (#3921) * fix: gcformat space * fix: gcformat continuous sign use hash * fix: delete incorrect comments --- cases/query/feature_signature_query.yaml | 80 +++++++++++-------- .../udf/default_defs/feature_signature_def.cc | 17 +++- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/cases/query/feature_signature_query.yaml b/cases/query/feature_signature_query.yaml index 1cfbd9b229a..c0763d320e7 100644 --- a/cases/query/feature_signature_query.yaml +++ b/cases/query/feature_signature_query.yaml @@ -43,7 +43,7 @@ cases: mode: procedure-unsupport db: db1 sql: | - select gcformat( + select concat("#", gcformat( discrete(3, -1), discrete(3, 0), discrete(3, int("null")), @@ -57,31 +57,31 @@ cases: discrete(-1, 5), discrete(-2, 5), discrete(-3, 5), - discrete(-4, 5)) as instance, + discrete(-4, 5))) as instance; expect: schema: instance:string data: | - | 4:628 5:491882390849628 6:0 7:4 8:1 9:3 10:1 11:1 12:0 13:0 14:4 + # | 4:628 5:491882390849628 6:0 7:4 8:1 9:3 10:1 11:1 12:0 13:0 14:4 - id: 2 desc: feature signature select GCFormat no label mode: procedure-unsupport db: db1 sql: | - select gcformat( + select concat("#", gcformat( discrete(hash64("x"), 1), continuous(pow(10, 30)), continuous(-pow(10, 1000)), - continuous(abs(sqrt(-1)))) as instance; + continuous(abs(sqrt(-1))))) as instance; expect: schema: instance:string data: | - | 1:0 2:0:1000000000000000019884624838656.000000 3:0:-inf 4:0:nan + # | 1:0 2:3353244675891348105:1000000000000000019884624838656.000000 3:7262150054277104024:-inf 4:3255232038643208583:nan - id: 3 desc: feature signature GCFormat null mode: procedure-unsupport db: db1 sql: | - select gcformat( + select concat("#", gcformat( regression_label(2), regression_label(int("null")), continuous(int("null")), @@ -98,31 +98,31 @@ cases: discrete(3, -100), discrete(3), continuous(0.0), - continuous(int("null"))) as instance; + continuous(int("null")))) as instance; expect: schema: instance:string data: | - | 3:0:-1 4:0:2681491882390849628 5:28 8:2681491882390849628 9:0:-1 10:28 13:2681491882390849628 14:0:0.000000 + # | 3:7262150054277104024:-1 4:3255232038643208583:2681491882390849628 5:28 8:2681491882390849628 9:-7745589761753622095:-1 10:28 13:2681491882390849628 14:398281081943027035:0.000000 - id: 4 desc: feature signature GCFormat no feature mode: procedure-unsupport db: db1 sql: | - select gcformat(binary_label(false)); + select concat(gcformat(binary_label(false)), "#") as instance; expect: - schema: gcformat(binary_label(false)):string + schema: instance:string data: | - 0| + 0 | # - id: 5 desc: feature signature GCFormat nothing mode: procedure-unsupport db: db1 sql: | - select gcformat(); + select concat(concat("#", gcformat()), "#") as instance; expect: - schema: gcformat():string + schema: instance:string data: | - | + # | # - id: 6 desc: feature signature CSV no label mode: procedure-unsupport @@ -136,7 +136,7 @@ cases: expect: columns: [instance:string] rows: - - [",,,628"] + - [ ",,,628" ] - id: 7 desc: feature signature CSV null mode: procedure-unsupport @@ -163,7 +163,7 @@ cases: expect: columns: [ "instance:string "] rows: - - ["2,,,,-1,2681491882390849628,28,,,2681491882390849628,-1,28,,,2681491882390849628,0.000000,"] + - [ "2,,,,-1,2681491882390849628,28,,,2681491882390849628,-1,28,,,2681491882390849628,0.000000," ] - id: 8 desc: feature signature CSV no feature mode: procedure-unsupport @@ -263,7 +263,7 @@ cases: expect: schema: instance:string data: | - 1| 1:0:0 2:0:1 3:0 + 1 | 1:5925585971146611297:0 2:3353244675891348105:1 3:0 - id: 15 desc: feature signature select GCFormat from mode: request-unsupport @@ -289,11 +289,11 @@ cases: schema: instance:string order: instance data: | - 1| 1:0:0 2:0:1 3:0 - 2| 1:0:0 2:0:2 3:0 - 3| 1:0:1 2:0:3 3:0 - 4| 1:0:1 2:0:4 3:0 - 5| 1:0:2 2:0:5 3:0 + 1 | 1:5925585971146611297:0 2:3353244675891348105:1 3:0 + 2 | 1:5925585971146611297:0 2:3353244675891348105:2 3:0 + 3 | 1:5925585971146611297:1 2:3353244675891348105:3 3:0 + 4 | 1:5925585971146611297:1 2:3353244675891348105:4 3:0 + 5 | 1:5925585971146611297:2 2:3353244675891348105:5 3:0 - id: 16 desc: feature signature select CSV from mode: request-unsupport @@ -360,7 +360,7 @@ cases: mode: request-unsupport db: db1 sql: | - SELECT gcformat(regression_label(col1)) as col1, + SELECT gcformat(regression_label(col1), discrete(col1, 1)) as col1, csv(regression_label(col1)) as col2, libsvm(regression_label(col1)) as col3 FROM t1; @@ -375,14 +375,14 @@ cases: 1, 4, 55, 4.4, 44.4, 2, 4444 2, 5, 55, 5.5, 55.5, 3, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa expect: - schema: col1:string, col2:string, col3:string - order: col1 - data: | - 1|, 1, 1 - 2|, 2, 2 - 3|, 3, 3 - 4|, 4, 4 - 5|, 5, 5 + columns: [ "col1:string", "col2:string", "col3:string" ] + order: "col1" + rows: + - [ "1 | 1:0", "1", "1" ] + - [ "2 | 1:0", "2", "2" ] + - [ "3 | 1:0", "3", "3" ] + - [ "4 | 1:0", "4", "4" ] + - [ "5 | 1:0", "5", "5" ] - id: 19 desc: feature signature select from join mode: request-unsupport @@ -471,15 +471,25 @@ cases: mode: procedure-unsupport db: db1 sql: | - select gcformat( + select concat("#", gcformat( regression_label(2), continuous(1), continuous(int("notint")), continuous(0), continuous(0.0), discrete(3), - regression_label(int("notint"))) as instance; + regression_label(int("notint")))) as instance; expect: schema: instance:string data: | - | 1:0:1 3:0:0 4:0:0.000000 5:2681491882390849628 + # | 1:5925585971146611297:1 3:7262150054277104024:0 4:3255232038643208583:0.000000 5:2681491882390849628 + - id: 23 + desc: hash64 + mode: procedure-unsupport + db: db1 + sql: | + select hash64(3) as col1, hash64(bigint(3)) as col2; + expect: + schema: col1:int64, col2:int64 + data: | + 2681491882390849628, 7262150054277104024 diff --git a/hybridse/src/udf/default_defs/feature_signature_def.cc b/hybridse/src/udf/default_defs/feature_signature_def.cc index 3f9586c7f61..b407d513bb4 100644 --- a/hybridse/src/udf/default_defs/feature_signature_def.cc +++ b/hybridse/src/udf/default_defs/feature_signature_def.cc @@ -204,14 +204,23 @@ struct GCFormat { switch (feature_signature) { case kFeatureSignatureContinuous: { if (!is_null) { - instance_feature += " " + std::to_string(slot_number) + ":0:" + format_continuous(input); + if (!instance_feature.empty()) { + instance_feature += " "; + } + int64_t hash = FarmFingerprint(CCallDataTypeTrait::to_bytes_ref(&slot_number)); + instance_feature += std::to_string(slot_number) + ":"; + instance_feature += format_discrete(hash); + instance_feature += ":" + format_continuous(input); } ++slot_number; break; } case kFeatureSignatureDiscrete: { if (!is_null) { - instance_feature += " " + std::to_string(slot_number) + ":" + format_discrete(input); + if (!instance_feature.empty()) { + instance_feature += " "; + } + instance_feature += std::to_string(slot_number) + ":" + format_discrete(input); } ++slot_number; break; @@ -249,7 +258,7 @@ struct GCFormat { } std::string Output() { - return instance_label + "|" + instance_feature; + return instance_label + " | " + instance_feature; } size_t slot_number = 1; @@ -482,7 +491,7 @@ void DefaultUdfLibrary::InitFeatureSignature() { Example: @code{.sql} select gcformat(multiclass_label(6), continuous(1.5), category(3)); - -- output 6| 1:0:1.500000 2:2681491882390849628 + -- output 6 | 1:0:1.500000 2:2681491882390849628 @endcode @since 0.9.0 From ba817e44c5ca5e199befb080f2aa403e1c9db66a Mon Sep 17 00:00:00 2001 From: tobe Date: Thu, 16 May 2024 14:23:57 +0800 Subject: [PATCH 12/41] feat: merge 090 features to main (#3929) * Set s3 and aws dependencies ad provided (#3897) * feat: execlude zookeeper for curator (#3899) * Execlude zookeeper when using curator * Fix local build java --- java/openmldb-batch/pom.xml | 13 ++++++++++- java/openmldb-common/pom.xml | 6 +++++ java/openmldb-taskmanager/pom.xml | 9 +++++--- .../taskmanager/server/JobResultSaver.java | 22 +++++++++---------- .../taskmanager/zk/RecoverableZooKeeper.java | 2 +- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/java/openmldb-batch/pom.xml b/java/openmldb-batch/pom.xml index b69f58abc2b..5fcbca9a8f1 100644 --- a/java/openmldb-batch/pom.xml +++ b/java/openmldb-batch/pom.xml @@ -167,7 +167,11 @@ - + + org.apache.zookeeper + zookeeper + 3.4.14 + org.apache.curator curator-framework @@ -182,6 +186,12 @@ org.apache.curator curator-recipes 4.2.0 + + + org.apache.zookeeper + zookeeper + + @@ -241,6 +251,7 @@ org.apache.hadoop hadoop-aws ${hadoop.version} + provided diff --git a/java/openmldb-common/pom.xml b/java/openmldb-common/pom.xml index d19b9cac681..afa86a5a0dc 100644 --- a/java/openmldb-common/pom.xml +++ b/java/openmldb-common/pom.xml @@ -40,6 +40,12 @@ org.apache.curator curator-recipes 4.2.0 + + + org.apache.zookeeper + zookeeper + + org.testng diff --git a/java/openmldb-taskmanager/pom.xml b/java/openmldb-taskmanager/pom.xml index 34039fb642a..1b0fe69928e 100644 --- a/java/openmldb-taskmanager/pom.xml +++ b/java/openmldb-taskmanager/pom.xml @@ -134,6 +134,12 @@ org.apache.curator curator-recipes 4.2.0 + + + org.apache.zookeeper + zookeeper + + org.projectlombok @@ -142,9 +148,6 @@ provided - - - io.fabric8 diff --git a/java/openmldb-taskmanager/src/main/java/com/_4paradigm/openmldb/taskmanager/server/JobResultSaver.java b/java/openmldb-taskmanager/src/main/java/com/_4paradigm/openmldb/taskmanager/server/JobResultSaver.java index 570bc035603..0e9825d0423 100644 --- a/java/openmldb-taskmanager/src/main/java/com/_4paradigm/openmldb/taskmanager/server/JobResultSaver.java +++ b/java/openmldb-taskmanager/src/main/java/com/_4paradigm/openmldb/taskmanager/server/JobResultSaver.java @@ -53,7 +53,7 @@ */ @Slf4j public class JobResultSaver { - private static final Log log = LogFactory.getLog(JobResultSaver.class); + private static final Log logger = LogFactory.getLog(JobResultSaver.class); // false: unused, true: using // 0: unused, 1: saving, 2: finished but still in use @@ -92,8 +92,8 @@ public String genUniqueFileName() { public boolean saveFile(int resultId, String jsonData) { // No need to wait, cuz id status must have been changed by genResultId before. // It's a check. - if (log.isDebugEnabled()) { - log.debug("save result " + resultId + ", data " + jsonData); + if (logger.isDebugEnabled()) { + logger.debug("save result " + resultId + ", data " + jsonData); } int status = idStatus.get(resultId); if (status != 1) { @@ -105,7 +105,7 @@ public boolean saveFile(int resultId, String jsonData) { idStatus.set(resultId, 2); idStatus.notifyAll(); } - log.info("saved all result of result " + resultId); + logger.info("saved all result of result " + resultId); return true; } // save to /tmp_result// @@ -114,7 +114,7 @@ public boolean saveFile(int resultId, String jsonData) { File saveP = new File(savePath); if (!saveP.exists()) { boolean res = saveP.mkdirs(); - log.info("create save path " + savePath + ", status " + res); + logger.info("create save path " + savePath + ", status " + res); } } String fileFullPath = String.format("%s/%s", savePath, genUniqueFileName()); @@ -125,7 +125,7 @@ public boolean saveFile(int resultId, String jsonData) { + fileFullPath); } } catch (IOException e) { - log.error("create file failed, path " + fileFullPath, e); + logger.error("create file failed, path " + fileFullPath, e); return false; } @@ -135,7 +135,7 @@ public boolean saveFile(int resultId, String jsonData) { } catch (IOException e) { // Write failed, we'll lost a part of result, but it's ok for show sync job // output. So we just log it, and response the http request. - log.error("write result to file failed, path " + fileFullPath, e); + logger.error("write result to file failed, path " + fileFullPath, e); return false; } return true; @@ -151,7 +151,7 @@ public String readResult(int resultId, long timeoutMs) throws InterruptedExcepti } } if (idStatus.get(resultId) != 2) { - log.warn("read result timeout, result saving may be still running, try read anyway, id " + resultId); + logger.warn("read result timeout, result saving may be still running, try read anyway, id " + resultId); } String output = ""; // all finished, read csv from savePath @@ -163,7 +163,7 @@ public String readResult(int resultId, long timeoutMs) throws InterruptedExcepti output = printFilesTostr(savePath); FileUtils.forceDelete(saveP); } else { - log.info("empty result for " + resultId + ", show empty string"); + logger.info("empty result for " + resultId + ", show empty string"); } // reset id synchronized (idStatus) { @@ -189,7 +189,7 @@ public String printFilesTostr(String fileDir) { } return stringWriter.toString(); } catch (Exception e) { - log.warn("read result met exception when read " + fileDir + ", " + e.getMessage()); + logger.warn("read result met exception when read " + fileDir + ", " + e.getMessage()); e.printStackTrace(); return "read met exception, check the taskmanager log"; } @@ -219,7 +219,7 @@ private void printFile(String file, StringWriter stringWriter, boolean printHead csvPrinter.printRecord(iter.next()); } } catch (Exception e) { - log.warn("error when print result file " + file + ", ignore it"); + logger.warn("error when print result file " + file + ", ignore it"); e.printStackTrace(); } } diff --git a/java/openmldb-taskmanager/src/main/java/com/_4paradigm/openmldb/taskmanager/zk/RecoverableZooKeeper.java b/java/openmldb-taskmanager/src/main/java/com/_4paradigm/openmldb/taskmanager/zk/RecoverableZooKeeper.java index 9ff2b9349b4..10bc226ef50 100644 --- a/java/openmldb-taskmanager/src/main/java/com/_4paradigm/openmldb/taskmanager/zk/RecoverableZooKeeper.java +++ b/java/openmldb-taskmanager/src/main/java/com/_4paradigm/openmldb/taskmanager/zk/RecoverableZooKeeper.java @@ -62,7 +62,7 @@ public class RecoverableZooKeeper { private final String quorumServers; private final int maxMultiSize; // unused now - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "DE_MIGHT_IGNORE", justification = "None. Its always been this way.") + //@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "DE_MIGHT_IGNORE", justification = "None. Its always been this way.") public RecoverableZooKeeper(String quorumServers, int sessionTimeout, Watcher watcher) throws IOException { // TODO: Add support for zk 'chroot'; we don't add it to the quorumServers // String as we should. From 5bbf9e34fa39b45eb695e40dcce6a17001339b0d Mon Sep 17 00:00:00 2001 From: tobe Date: Fri, 17 May 2024 17:43:25 +0800 Subject: [PATCH 13/41] Run script to update post release version (#3931) --- CMakeLists.txt | 4 ++-- java/hybridse-native/pom.xml | 2 +- java/hybridse-proto/pom.xml | 2 +- java/hybridse-sdk/pom.xml | 2 +- java/openmldb-batch/pom.xml | 2 +- java/openmldb-batchjob/pom.xml | 2 +- java/openmldb-common/pom.xml | 2 +- java/openmldb-jdbc/pom.xml | 2 +- java/openmldb-native/pom.xml | 2 +- java/openmldb-spark-connector/pom.xml | 2 +- java/openmldb-synctool/pom.xml | 2 +- java/openmldb-taskmanager/pom.xml | 2 +- java/pom.xml | 4 ++-- python/openmldb_sdk/setup.py | 2 +- python/openmldb_tool/setup.py | 2 +- 15 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ac375c1d9e..7fc334f8566 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,8 +40,8 @@ endif() message (STATUS "CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}") message (STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") set(OPENMLDB_VERSION_MAJOR 0) -set(OPENMLDB_VERSION_MINOR 8) -set(OPENMLDB_VERSION_BUG 6) +set(OPENMLDB_VERSION_MINOR 9) +set(OPENMLDB_VERSION_BUG 1) function(get_commitid CODE_DIR COMMIT_ID) find_package(Git REQUIRED) diff --git a/java/hybridse-native/pom.xml b/java/hybridse-native/pom.xml index 632f3fe04a4..ba85e0169a0 100644 --- a/java/hybridse-native/pom.xml +++ b/java/hybridse-native/pom.xml @@ -5,7 +5,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/java/hybridse-proto/pom.xml b/java/hybridse-proto/pom.xml index e179740a4ec..4bd333cb322 100644 --- a/java/hybridse-proto/pom.xml +++ b/java/hybridse-proto/pom.xml @@ -4,7 +4,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/java/hybridse-sdk/pom.xml b/java/hybridse-sdk/pom.xml index 34d9e34e37e..ed8911fa572 100644 --- a/java/hybridse-sdk/pom.xml +++ b/java/hybridse-sdk/pom.xml @@ -6,7 +6,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/java/openmldb-batch/pom.xml b/java/openmldb-batch/pom.xml index 5fcbca9a8f1..8c0371e227a 100644 --- a/java/openmldb-batch/pom.xml +++ b/java/openmldb-batch/pom.xml @@ -7,7 +7,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT openmldb-batch diff --git a/java/openmldb-batchjob/pom.xml b/java/openmldb-batchjob/pom.xml index 101758c60dc..e449320d012 100644 --- a/java/openmldb-batchjob/pom.xml +++ b/java/openmldb-batchjob/pom.xml @@ -7,7 +7,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT openmldb-batchjob diff --git a/java/openmldb-common/pom.xml b/java/openmldb-common/pom.xml index afa86a5a0dc..6be5746496c 100644 --- a/java/openmldb-common/pom.xml +++ b/java/openmldb-common/pom.xml @@ -5,7 +5,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT 4.0.0 openmldb-common diff --git a/java/openmldb-jdbc/pom.xml b/java/openmldb-jdbc/pom.xml index dca8db69e79..f51395b9ac0 100644 --- a/java/openmldb-jdbc/pom.xml +++ b/java/openmldb-jdbc/pom.xml @@ -5,7 +5,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/java/openmldb-native/pom.xml b/java/openmldb-native/pom.xml index 214a918f7b6..ed3c45fae8b 100644 --- a/java/openmldb-native/pom.xml +++ b/java/openmldb-native/pom.xml @@ -5,7 +5,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT ../pom.xml 4.0.0 diff --git a/java/openmldb-spark-connector/pom.xml b/java/openmldb-spark-connector/pom.xml index 574e8ddbf84..529618163e0 100644 --- a/java/openmldb-spark-connector/pom.xml +++ b/java/openmldb-spark-connector/pom.xml @@ -6,7 +6,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT openmldb-spark-connector diff --git a/java/openmldb-synctool/pom.xml b/java/openmldb-synctool/pom.xml index d752ce6d41b..bbdb1aa1fa8 100644 --- a/java/openmldb-synctool/pom.xml +++ b/java/openmldb-synctool/pom.xml @@ -6,7 +6,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT openmldb-synctool openmldb-synctool diff --git a/java/openmldb-taskmanager/pom.xml b/java/openmldb-taskmanager/pom.xml index 1b0fe69928e..6fee727ff3e 100644 --- a/java/openmldb-taskmanager/pom.xml +++ b/java/openmldb-taskmanager/pom.xml @@ -6,7 +6,7 @@ openmldb-parent com.4paradigm.openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT openmldb-taskmanager openmldb-taskmanager diff --git a/java/pom.xml b/java/pom.xml index 7435a8cdfee..999ae2b8bae 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ openmldb-parent pom openmldb - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT hybridse-sdk hybridse-native @@ -65,7 +65,7 @@ - 0.8.6-SNAPSHOT + 0.9.1-SNAPSHOT error 2.9.0 diff --git a/python/openmldb_sdk/setup.py b/python/openmldb_sdk/setup.py index fa92ff71911..c682cc0c49f 100644 --- a/python/openmldb_sdk/setup.py +++ b/python/openmldb_sdk/setup.py @@ -18,7 +18,7 @@ setup( name='openmldb', - version='0.8.6a0', + version='0.9.1a0', author='OpenMLDB Team', author_email=' ', url='https://github.com/4paradigm/OpenMLDB', diff --git a/python/openmldb_tool/setup.py b/python/openmldb_tool/setup.py index e36856f8d37..d43a21c1c70 100644 --- a/python/openmldb_tool/setup.py +++ b/python/openmldb_tool/setup.py @@ -18,7 +18,7 @@ setup( name="openmldb-tool", - version='0.8.6a0', + version='0.9.1a0', author="OpenMLDB Team", author_email=" ", url="https://github.com/4paradigm/OpenMLDB", From 21184d56251cd96088d787dfdb32527c84c78467 Mon Sep 17 00:00:00 2001 From: oh2024 <162292688+oh2024@users.noreply.github.com> Date: Mon, 20 May 2024 14:49:14 +0800 Subject: [PATCH 14/41] feat: crud users synchronously (#3928) * fix: make clients use auth by default * fix: let skip auth flag only affect verify * feat: tablets get user table remotely * fix: use FLAGS_system_table_replica_num for user table * feat: consistent user cruds * fix: pass instance of tablet and nameserver into auth lambda to allow locking * feat: best effort try to flush user data to all tablets * fix: lock scope * fix: stop user sync thread safely * fix: default values for user table columns --- src/auth/user_access_manager.cc | 13 ++--- src/auth/user_access_manager.h | 6 +- src/base/status.h | 5 +- src/client/ns_client.cc | 28 +++++++++ src/client/ns_client.h | 4 ++ src/client/tablet_client.cc | 12 ++++ src/client/tablet_client.h | 2 + src/cmd/openmldb.cc | 13 ++--- src/cmd/sql_cmd_test.cc | 3 - src/nameserver/name_server_impl.cc | 92 +++++++++++++++++++++++++----- src/nameserver/name_server_impl.h | 16 +++++- src/proto/name_server.proto | 15 +++++ src/proto/tablet.proto | 3 + src/sdk/mini_cluster.h | 83 +++------------------------ src/sdk/sql_cluster_router.cc | 64 +++++++++++---------- src/tablet/tablet_impl.cc | 14 ++++- src/tablet/tablet_impl.h | 8 ++- 17 files changed, 232 insertions(+), 149 deletions(-) diff --git a/src/auth/user_access_manager.cc b/src/auth/user_access_manager.cc index 32506d8cbcf..d668a7dc497 100644 --- a/src/auth/user_access_manager.cc +++ b/src/auth/user_access_manager.cc @@ -32,20 +32,17 @@ UserAccessManager::UserAccessManager(IteratorFactory iterator_factory) UserAccessManager::~UserAccessManager() { StopSyncTask(); } void UserAccessManager::StartSyncTask() { - sync_task_running_ = true; - sync_task_thread_ = std::thread([this] { - while (sync_task_running_) { + sync_task_thread_ = std::thread([this, fut = stop_promise_.get_future()] { + while (true) { SyncWithDB(); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (fut.wait_for(std::chrono::minutes(15)) != std::future_status::timeout) return; } }); } void UserAccessManager::StopSyncTask() { - sync_task_running_ = false; - if (sync_task_thread_.joinable()) { - sync_task_thread_.join(); - } + stop_promise_.set_value(); + sync_task_thread_.join(); } void UserAccessManager::SyncWithDB() { diff --git a/src/auth/user_access_manager.h b/src/auth/user_access_manager.h index af2dc0c6791..996efc326c4 100644 --- a/src/auth/user_access_manager.h +++ b/src/auth/user_access_manager.h @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -38,14 +39,13 @@ class UserAccessManager { ~UserAccessManager(); bool IsAuthenticated(const std::string& host, const std::string& username, const std::string& password); + void SyncWithDB(); private: IteratorFactory user_table_iterator_factory_; RefreshableMap user_map_; - std::atomic sync_task_running_{false}; std::thread sync_task_thread_; - - void SyncWithDB(); + std::promise stop_promise_; void StartSyncTask(); void StopSyncTask(); }; diff --git a/src/base/status.h b/src/base/status.h index 8ac134b18bd..c7e5ec75198 100644 --- a/src/base/status.h +++ b/src/base/status.h @@ -183,7 +183,10 @@ enum ReturnCode { kSQLRunError = 1001, kRPCRunError = 1002, kServerConnError = 1003, - kRPCError = 1004 // brpc controller error + kRPCError = 1004, // brpc controller error + + // auth + kFlushPrivilegesFailed = 1100 // brpc controller error }; struct Status { diff --git a/src/client/ns_client.cc b/src/client/ns_client.cc index 9a4baa549bc..cdeef07e521 100644 --- a/src/client/ns_client.cc +++ b/src/client/ns_client.cc @@ -19,6 +19,7 @@ #include #include "base/strings.h" +#include "ns_client.h" DECLARE_int32(request_timeout_ms); namespace openmldb { @@ -302,6 +303,33 @@ bool NsClient::CreateTable(const ::openmldb::nameserver::TableInfo& table_info, bool NsClient::DropTable(const std::string& name, std::string& msg) { return DropTable(GetDb(), name, msg); } +bool NsClient::PutUser(const std::string& host, const std::string& name, const std::string& password) { + ::openmldb::nameserver::PutUserRequest request; + request.set_host(host); + request.set_name(name); + request.set_password(password); + ::openmldb::nameserver::GeneralResponse response; + bool ok = client_.SendRequest(&::openmldb::nameserver::NameServer_Stub::PutUser, &request, &response, + FLAGS_request_timeout_ms, 1); + if (ok && response.code() == 0) { + return true; + } + return false; +} + +bool NsClient::DeleteUser(const std::string& host, const std::string& name) { + ::openmldb::nameserver::DeleteUserRequest request; + request.set_host(host); + request.set_name(name); + ::openmldb::nameserver::GeneralResponse response; + bool ok = client_.SendRequest(&::openmldb::nameserver::NameServer_Stub::DeleteUser, &request, &response, + FLAGS_request_timeout_ms, 1); + if (ok && response.code() == 0) { + return true; + } + return false; +} + bool NsClient::DropTable(const std::string& db, const std::string& name, std::string& msg) { ::openmldb::nameserver::DropTableRequest request; request.set_name(name); diff --git a/src/client/ns_client.h b/src/client/ns_client.h index 15a19f48ae7..73a52854765 100644 --- a/src/client/ns_client.h +++ b/src/client/ns_client.h @@ -110,6 +110,10 @@ class NsClient : public Client { bool DropTable(const std::string& name, std::string& msg); // NOLINT + bool PutUser(const std::string& host, const std::string& name, const std::string& password); // NOLINT + + bool DeleteUser(const std::string& host, const std::string& name); // NOLINT + bool DropTable(const std::string& db, const std::string& name, std::string& msg); // NOLINT diff --git a/src/client/tablet_client.cc b/src/client/tablet_client.cc index 09e7bdcbd96..a1dd925fcde 100644 --- a/src/client/tablet_client.cc +++ b/src/client/tablet_client.cc @@ -26,6 +26,7 @@ #include "codec/sql_rpc_row_codec.h" #include "common/timer.h" #include "sdk/sql_request_row.h" +#include "tablet_client.h" DECLARE_int32(request_max_retry); DECLARE_int32(request_timeout_ms); @@ -1414,5 +1415,16 @@ bool TabletClient::GetAndFlushDeployStats(::openmldb::api::DeployStatsResponse* return ok && res->code() == 0; } +bool TabletClient::FlushPrivileges() { + ::openmldb::api::EmptyRequest request; + ::openmldb::api::GeneralResponse response; + + bool ok = client_.SendRequest(&::openmldb::api::TabletServer_Stub::FlushPrivileges, &request, &response, + FLAGS_request_timeout_ms, 1); + if (ok && response.code() == 0) { + return true; + } + return false; +} } // namespace client } // namespace openmldb diff --git a/src/client/tablet_client.h b/src/client/tablet_client.h index 66155c968d7..177124208fc 100644 --- a/src/client/tablet_client.h +++ b/src/client/tablet_client.h @@ -267,6 +267,8 @@ class TabletClient : public Client { bool GetAndFlushDeployStats(::openmldb::api::DeployStatsResponse* res); + bool FlushPrivileges(); + private: base::Status LoadTableInternal(const ::openmldb::api::TableMeta& table_meta, std::shared_ptr task_info); diff --git a/src/cmd/openmldb.cc b/src/cmd/openmldb.cc index b13694d8d3c..3b3aa38cb5d 100644 --- a/src/cmd/openmldb.cc +++ b/src/cmd/openmldb.cc @@ -38,7 +38,6 @@ #endif #include "apiserver/api_server_impl.h" #include "auth/brpc_authenticator.h" -#include "auth/user_access_manager.h" #include "boost/algorithm/string.hpp" #include "boost/lexical_cast.hpp" #include "brpc/server.h" @@ -147,12 +146,10 @@ void StartNameServer() { } brpc::ServerOptions options; - std::unique_ptr user_access_manager; std::unique_ptr server_authenticator; - user_access_manager = std::make_unique(name_server->GetSystemTableIterator()); server_authenticator = std::make_unique( - [&user_access_manager](const std::string& host, const std::string& username, const std::string& password) { - return user_access_manager->IsAuthenticated(host, username, password); + [name_server](const std::string& host, const std::string& username, const std::string& password) { + return name_server->IsAuthenticated(host, username, password); }); options.auth = server_authenticator.get(); @@ -253,13 +250,11 @@ void StartTablet() { exit(1); } brpc::ServerOptions options; - std::unique_ptr user_access_manager; std::unique_ptr server_authenticator; - user_access_manager = std::make_unique(tablet->GetSystemTableIterator()); server_authenticator = std::make_unique( - [&user_access_manager](const std::string& host, const std::string& username, const std::string& password) { - return user_access_manager->IsAuthenticated(host, username, password); + [tablet](const std::string& host, const std::string& username, const std::string& password) { + return tablet->IsAuthenticated(host, username, password); }); options.auth = server_authenticator.get(); options.num_threads = FLAGS_thread_pool_size; diff --git a/src/cmd/sql_cmd_test.cc b/src/cmd/sql_cmd_test.cc index cedda42a6cd..fe8faa21504 100644 --- a/src/cmd/sql_cmd_test.cc +++ b/src/cmd/sql_cmd_test.cc @@ -245,7 +245,6 @@ TEST_P(DBSDKTest, TestUser) { ASSERT_TRUE(status.IsOK()); ASSERT_TRUE(true); auto opt = sr->GetRouterOptions(); - std::this_thread::sleep_for(std::chrono::seconds(1)); // TODO(oh2024): Remove when CREATE USER becomes strongly if (cs->IsClusterMode()) { auto real_opt = std::dynamic_pointer_cast(opt); sdk::SQLRouterOptions opt1; @@ -257,7 +256,6 @@ TEST_P(DBSDKTest, TestUser) { ASSERT_TRUE(router != nullptr); sr->ExecuteSQL(absl::StrCat("ALTER USER user1 SET OPTIONS(password='abc')"), &status); ASSERT_TRUE(status.IsOK()); - std::this_thread::sleep_for(std::chrono::seconds(1)); // TODO(oh2024): Remove when CREATE USER becomes strongly router = NewClusterSQLRouter(opt1); ASSERT_FALSE(router != nullptr); } else { @@ -271,7 +269,6 @@ TEST_P(DBSDKTest, TestUser) { ASSERT_TRUE(router != nullptr); sr->ExecuteSQL(absl::StrCat("ALTER USER user1 SET OPTIONS(password='abc')"), &status); ASSERT_TRUE(status.IsOK()); - std::this_thread::sleep_for(std::chrono::seconds(1)); // TODO(oh2024): Remove when CREATE USER becomes strongly router = NewStandaloneSQLRouter(opt1); ASSERT_FALSE(router != nullptr); } diff --git a/src/nameserver/name_server_impl.cc b/src/nameserver/name_server_impl.cc index 871adcb8d49..9c565272fb3 100644 --- a/src/nameserver/name_server_impl.cc +++ b/src/nameserver/name_server_impl.cc @@ -45,6 +45,7 @@ #include "boost/bind.hpp" #include "codec/row_codec.h" #include "gflags/gflags.h" +#include "name_server_impl.h" #include "schema/index_util.h" #include "schema/schema_adapter.h" @@ -522,7 +523,8 @@ NameServerImpl::NameServerImpl() thread_pool_(1), task_thread_pool_(FLAGS_name_server_task_pool_size), rand_(0xdeadbeef), - startup_mode_(::openmldb::type::StartupMode::kStandalone) {} + startup_mode_(::openmldb::type::StartupMode::kStandalone), + user_access_manager_(GetSystemTableIterator()) {} NameServerImpl::~NameServerImpl() { running_.store(false, std::memory_order_release); @@ -650,7 +652,7 @@ bool NameServerImpl::Recover() { if (!RecoverExternalFunction()) { return false; } - return true; + return FlushPrivileges().OK(); } bool NameServerImpl::RecoverExternalFunction() { @@ -1377,8 +1379,8 @@ void NameServerImpl::ShowTablet(RpcController* controller, const ShowTabletReque response->set_msg("ok"); } -base::Status NameServerImpl::InsertUserRecord(const std::string& host, const std::string& user, - const std::string& password) { +base::Status NameServerImpl::PutUserRecord(const std::string& host, const std::string& user, + const std::string& password) { std::shared_ptr table_info; if (!GetTableInfo(USER_INFO_NAME, INTERNAL_DB, &table_info)) { return {ReturnCode::kTableIsNotExist, "user table does not exist"}; @@ -1388,13 +1390,13 @@ base::Status NameServerImpl::InsertUserRecord(const std::string& host, const std row_values.push_back(host); row_values.push_back(user); row_values.push_back(password); - row_values.push_back(""); // password_last_changed - row_values.push_back(""); // password_expired_time - row_values.push_back(""); // create_time - row_values.push_back(""); // update_time - row_values.push_back(""); // account_type - row_values.push_back(""); // privileges - row_values.push_back(""); // extra_info + row_values.push_back("0"); // password_last_changed + row_values.push_back("0"); // password_expired_time + row_values.push_back("0"); // create_time + row_values.push_back("0"); // update_time + row_values.push_back("1"); // account_type + row_values.push_back("0"); // privileges + row_values.push_back("null"); // extra_info std::string encoded_row; codec::RowCodec::EncodeRow(row_values, table_info->column_desc(), 1, encoded_row); @@ -1410,11 +1412,56 @@ base::Status NameServerImpl::InsertUserRecord(const std::string& host, const std std::string endpoint = table_partition.partition_meta(meta_idx).endpoint(); auto table_ptr = GetTablet(endpoint); if (!table_ptr->client_->Put(tid, 0, cur_ts, encoded_row, dimensions).OK()) { - return {ReturnCode::kPutFailed, "failed to create initial user entry"}; + return {ReturnCode::kPutFailed, "failed to put user entry"}; } break; } } + return FlushPrivileges(); +} + +base::Status NameServerImpl::DeleteUserRecord(const std::string& host, const std::string& user) { + std::shared_ptr table_info; + if (!GetTableInfo(USER_INFO_NAME, INTERNAL_DB, &table_info)) { + return {ReturnCode::kTableIsNotExist, "user table does not exist"}; + } + uint32_t tid = table_info->tid(); + auto table_partition = table_info->table_partition(0); // only one partition for system table + std::string msg; + for (int meta_idx = 0; meta_idx < table_partition.partition_meta_size(); meta_idx++) { + if (table_partition.partition_meta(meta_idx).is_leader() && + table_partition.partition_meta(meta_idx).is_alive()) { + uint64_t cur_ts = ::baidu::common::timer::get_micros() / 1000; + std::string endpoint = table_partition.partition_meta(meta_idx).endpoint(); + auto table_ptr = GetTablet(endpoint); + if (!table_ptr->client_->Delete(tid, 0, host + "|" + user, "index", msg)) { + return {ReturnCode::kDeleteFailed, msg}; + } + + break; + } + } + return FlushPrivileges(); +} + +base::Status NameServerImpl::FlushPrivileges() { + user_access_manager_.SyncWithDB(); + std::vector failed_tablet_list; + { + std::lock_guard lock(mu_); + for (const auto& tablet_pair : tablets_) { + const std::shared_ptr& tablet_info = tablet_pair.second; + if (tablet_info && tablet_info->Health() && tablet_info->client_) { + if (!tablet_info->client_->FlushPrivileges()) { + failed_tablet_list.push_back(tablet_pair.first); + } + } + } + } + if (failed_tablet_list.size() > 0) { + return {ReturnCode::kFlushPrivilegesFailed, + "Failed to flush privileges to tablets: " + boost::algorithm::join(failed_tablet_list, ", ")}; + } return {}; } @@ -5593,7 +5640,7 @@ void NameServerImpl::OnLocked() { CreateDatabaseOrExit(INTERNAL_DB); if (db_table_info_[INTERNAL_DB].count(USER_INFO_NAME) == 0) { CreateSystemTableOrExit(SystemTableType::kUser); - InsertUserRecord("%", "root", "1e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + PutUserRecord("%", "root", "1e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); } if (IsClusterMode()) { if (tablets_.size() < FLAGS_system_table_replica_num) { @@ -9613,6 +9660,25 @@ NameServerImpl::GetSystemTableIterator() { }; } +void NameServerImpl::PutUser(RpcController* controller, const PutUserRequest* request, GeneralResponse* response, + Closure* done) { + brpc::ClosureGuard done_guard(done); + auto status = PutUserRecord(request->host(), request->name(), request->password()); + base::SetResponseStatus(status, response); +} + +void NameServerImpl::DeleteUser(RpcController* controller, const DeleteUserRequest* request, GeneralResponse* response, + Closure* done) { + brpc::ClosureGuard done_guard(done); + auto status = DeleteUserRecord(request->host(), request->name()); + base::SetResponseStatus(status, response); +} + +bool NameServerImpl::IsAuthenticated(const std::string& host, const std::string& username, + const std::string& password) { + return user_access_manager_.IsAuthenticated(host, username, password); +} + bool NameServerImpl::RecoverProcedureInfo() { db_table_sp_map_.clear(); db_sp_table_map_.clear(); diff --git a/src/nameserver/name_server_impl.h b/src/nameserver/name_server_impl.h index 9960fd3d247..dadc335c7a3 100644 --- a/src/nameserver/name_server_impl.h +++ b/src/nameserver/name_server_impl.h @@ -29,6 +29,7 @@ #include #include +#include "auth/user_access_manager.h" #include "base/hash.h" #include "base/random.h" #include "catalog/distribute_iterator.h" @@ -358,15 +359,23 @@ class NameServerImpl : public NameServer { void DropProcedure(RpcController* controller, const api::DropProcedureRequest* request, GeneralResponse* response, Closure* done); + void PutUser(RpcController* controller, const PutUserRequest* request, GeneralResponse* response, Closure* done); + void DeleteUser(RpcController* controller, const DeleteUserRequest* request, GeneralResponse* response, + Closure* done); + bool IsAuthenticated(const std::string& host, const std::string& username, const std::string& password); + + private: + std::function, std::unique_ptr>>(const std::string& table_name)> GetSystemTableIterator(); - + bool GetTableInfo(const std::string& table_name, const std::string& db_name, std::shared_ptr* table_info); - private: - base::Status InsertUserRecord(const std::string& host, const std::string& user, const std::string& password); + base::Status PutUserRecord(const std::string& host, const std::string& user, const std::string& password); + base::Status DeleteUserRecord(const std::string& host, const std::string& user); + base::Status FlushPrivileges(); base::Status InitGlobalVarTable(); @@ -735,6 +744,7 @@ class NameServerImpl : public NameServer { std::unordered_map>> db_sp_info_map_; ::openmldb::type::StartupMode startup_mode_; + openmldb::auth::UserAccessManager user_access_manager_; }; } // namespace nameserver diff --git a/src/proto/name_server.proto b/src/proto/name_server.proto index c75dca8f5a9..f7c8fd5c830 100755 --- a/src/proto/name_server.proto +++ b/src/proto/name_server.proto @@ -533,6 +533,17 @@ message TableIndex { repeated openmldb.common.ColumnKey column_key = 3; } +message PutUserRequest { + required string host = 1; + required string name = 2; + required string password = 3; +} + +message DeleteUserRequest { + required string host = 1; + required string name = 2; +} + message DeploySQLRequest { optional openmldb.api.ProcedureInfo sp_info = 3; repeated TableIndex index = 4; @@ -602,4 +613,8 @@ service NameServer { rpc DropProcedure(openmldb.api.DropProcedureRequest) returns (GeneralResponse); rpc ShowProcedure(openmldb.api.ShowProcedureRequest) returns (openmldb.api.ShowProcedureResponse); rpc DeploySQL(DeploySQLRequest) returns (DeploySQLResponse); + + // user related interfaces + rpc PutUser(PutUserRequest) returns (GeneralResponse); + rpc DeleteUser(DeleteUserRequest) returns (GeneralResponse); } diff --git a/src/proto/tablet.proto b/src/proto/tablet.proto index a1ae6e72d5a..bc160a01f1e 100755 --- a/src/proto/tablet.proto +++ b/src/proto/tablet.proto @@ -977,4 +977,7 @@ service TabletServer { rpc CreateAggregator(CreateAggregatorRequest) returns (CreateAggregatorResponse); // monitoring interfaces rpc GetAndFlushDeployStats(GAFDeployStatsRequest) returns (DeployStatsResponse); + + // flush privilege + rpc FlushPrivileges(EmptyRequest) returns (GeneralResponse); } diff --git a/src/sdk/mini_cluster.h b/src/sdk/mini_cluster.h index 673e1cb1f61..24521005772 100644 --- a/src/sdk/mini_cluster.h +++ b/src/sdk/mini_cluster.h @@ -26,7 +26,6 @@ #include #include "auth/brpc_authenticator.h" -#include "auth/user_access_manager.h" #include "base/file_util.h" #include "base/glog_wrapper.h" #include "brpc/server.h" @@ -74,12 +73,6 @@ class MiniCluster { } ~MiniCluster() { - for (auto& tablet_user_access_manager : tablet_user_access_managers_) { - if (tablet_user_access_manager) { - delete tablet_user_access_manager; - tablet_user_access_manager = nullptr; - } - } for (auto& tablet_authenticator : tablet_authenticators_) { if (tablet_authenticator) { delete tablet_authenticator; @@ -87,11 +80,6 @@ class MiniCluster { } } - if (user_access_manager_) { - delete user_access_manager_; - user_access_manager_ = nullptr; - } - if (ns_authenticator_) { delete ns_authenticator_; ns_authenticator_ = nullptr; @@ -137,15 +125,9 @@ class MiniCluster { if (!ok) { return false; } - if (!nameserver->GetTableInfo(::openmldb::nameserver::USER_INFO_NAME, ::openmldb::nameserver::INTERNAL_DB, - &user_table_info_)) { - PDLOG(WARNING, "Failed to get table info for user table"); - return false; - } - user_access_manager_ = new openmldb::auth::UserAccessManager(nameserver->GetSystemTableIterator()); ns_authenticator_ = new openmldb::authn::BRPCAuthenticator( [this](const std::string& host, const std::string& username, const std::string& password) { - return user_access_manager_->IsAuthenticated(host, username, password); + return nameserver->IsAuthenticated(host, username, password); }); brpc::ServerOptions options; options.auth = ns_authenticator_; @@ -173,23 +155,12 @@ class MiniCluster { } void Close() { - for (auto& tablet_user_access_manager : tablet_user_access_managers_) { - if (tablet_user_access_manager) { - delete tablet_user_access_manager; - tablet_user_access_manager = nullptr; - } - } for (auto& tablet_authenticator : tablet_authenticators_) { if (tablet_authenticator) { delete tablet_authenticator; tablet_authenticator = nullptr; } } - if (user_access_manager_) { - delete user_access_manager_; - user_access_manager_ = nullptr; - } - if (ns_authenticator_) { delete ns_authenticator_; ns_authenticator_ = nullptr; @@ -244,13 +215,10 @@ class MiniCluster { return false; } - auto tablet_user_access_manager = new openmldb::auth::UserAccessManager(tablet->GetSystemTableIterator()); auto ts_authenticator = new openmldb::authn::BRPCAuthenticator( - [tablet_user_access_manager](const std::string& host, const std::string& username, - const std::string& password) { - return tablet_user_access_manager->IsAuthenticated(host, username, password); + [tablet](const std::string& host, const std::string& username, const std::string& password) { + return tablet->IsAuthenticated(host, username, password); }); - tablet_user_access_managers_.push_back(tablet_user_access_manager); tablet_authenticators_.push_back(ts_authenticator); brpc::ServerOptions options; options.auth = ts_authenticator; @@ -291,22 +259,13 @@ class MiniCluster { std::map tablets_; std::map tb_clients_; openmldb::authn::BRPCAuthenticator* ns_authenticator_; - openmldb::auth::UserAccessManager* user_access_manager_; - std::vector tablet_user_access_managers_; std::vector tablet_authenticators_; - std::shared_ptr<::openmldb::nameserver::TableInfo> user_table_info_; }; class StandaloneEnv { public: StandaloneEnv() : ns_(), ns_client_(nullptr), tb_client_(nullptr) { FLAGS_skip_grant_tables = false; } ~StandaloneEnv() { - for (auto& tablet_user_access_manager : tablet_user_access_managers_) { - if (tablet_user_access_manager) { - delete tablet_user_access_manager; - tablet_user_access_manager = nullptr; - } - } for (auto& tablet_authenticator : tablet_authenticators_) { if (tablet_authenticator) { delete tablet_authenticator; @@ -314,11 +273,6 @@ class StandaloneEnv { } } - if (user_access_manager_) { - delete user_access_manager_; - user_access_manager_ = nullptr; - } - if (ns_authenticator_) { delete ns_authenticator_; ns_authenticator_ = nullptr; @@ -353,15 +307,9 @@ class StandaloneEnv { if (!ok) { return false; } - if (!nameserver->GetTableInfo(::openmldb::nameserver::USER_INFO_NAME, ::openmldb::nameserver::INTERNAL_DB, - &user_table_info_)) { - PDLOG(WARNING, "Failed to get table info for user table"); - return false; - } - user_access_manager_ = new openmldb::auth::UserAccessManager(nameserver->GetSystemTableIterator()); ns_authenticator_ = new openmldb::authn::BRPCAuthenticator( [this](const std::string& host, const std::string& username, const std::string& password) { - return user_access_manager_->IsAuthenticated(host, username, password); + return nameserver->IsAuthenticated(host, username, password); }); brpc::ServerOptions options; options.auth = ns_authenticator_; @@ -387,12 +335,6 @@ class StandaloneEnv { } void Close() { - for (auto& tablet_user_access_manager : tablet_user_access_managers_) { - if (tablet_user_access_manager) { - delete tablet_user_access_manager; - tablet_user_access_manager = nullptr; - } - } for (auto& tablet_authenticator : tablet_authenticators_) { if (tablet_authenticator) { delete tablet_authenticator; @@ -400,11 +342,6 @@ class StandaloneEnv { } } - if (user_access_manager_) { - delete user_access_manager_; - user_access_manager_ = nullptr; - } - if (ns_authenticator_) { delete ns_authenticator_; ns_authenticator_ = nullptr; @@ -436,15 +373,12 @@ class StandaloneEnv { bool ok = tablet->Init("", "", tb_endpoint, ""); if (!ok) { return false; - } + } - auto tablet_user_access_manager = new openmldb::auth::UserAccessManager(tablet->GetSystemTableIterator()); auto ts_authenticator = new openmldb::authn::BRPCAuthenticator( - [tablet_user_access_manager](const std::string& host, const std::string& username, - const std::string& password) { - return tablet_user_access_manager->IsAuthenticated(host, username, password); + [tablet](const std::string& host, const std::string& username, const std::string& password) { + return tablet->IsAuthenticated(host, username, password); }); - tablet_user_access_managers_.push_back(tablet_user_access_manager); tablet_authenticators_.push_back(ts_authenticator); brpc::ServerOptions options; options.auth = ts_authenticator; @@ -474,9 +408,6 @@ class StandaloneEnv { ::openmldb::client::NsClient* ns_client_; ::openmldb::client::TabletClient* tb_client_; openmldb::authn::BRPCAuthenticator* ns_authenticator_; - openmldb::auth::UserAccessManager* user_access_manager_; - std::shared_ptr<::openmldb::nameserver::TableInfo> user_table_info_; - std::vector tablet_user_access_managers_; std::vector tablet_authenticators_; }; diff --git a/src/sdk/sql_cluster_router.cc b/src/sdk/sql_cluster_router.cc index 705fbd62400..e58eb8cd2cc 100644 --- a/src/sdk/sql_cluster_router.cc +++ b/src/sdk/sql_cluster_router.cc @@ -4902,49 +4902,51 @@ absl::StatusOr SQLClusterRouter::GetUser(const std::string& name, UserInfo hybridse::sdk::Status SQLClusterRouter::AddUser(const std::string& name, const std::string& password) { auto real_password = password.empty() ? password : codec::Encrypt(password); - uint64_t cur_ts = ::baidu::common::timer::get_micros() / 1000; - std::string sql = absl::StrCat("insert into ", nameserver::USER_INFO_NAME, " values (", - "'%',", // host - "'", name, "','", // user - real_password, "',", // password - cur_ts, ",", // password_last_changed - "0,", // password_expired_time - cur_ts, ", ", // create_time - cur_ts, ",", // update_time - 1, // account_type - ",'',", // privileges - "null" // extra_info - ");"); + hybridse::sdk::Status status; - ExecuteInsert(nameserver::INTERNAL_DB, sql, &status); + + auto ns_client = cluster_sdk_->GetNsClient(); + + bool ok = ns_client->PutUser("%", name, real_password); + + if (!ok) { + status.code = hybridse::common::StatusCode::kRunError; + status.msg = absl::StrCat("Fail to create user: ", name); + } + return status; } hybridse::sdk::Status SQLClusterRouter::UpdateUser(const UserInfo& user_info, const std::string& password) { + auto name = user_info.name; auto real_password = password.empty() ? password : codec::Encrypt(password); - uint64_t cur_ts = ::baidu::common::timer::get_micros() / 1000; - std::string sql = absl::StrCat("insert into ", nameserver::USER_INFO_NAME, " values (", - "'%',", // host - "'", user_info.name, "','", // user - real_password, "',", // password - cur_ts, ",", // password_last_changed - "0,", // password_expired_time - user_info.create_time, ", ", // create_time - cur_ts, ",", // update_time - 1, // account_type - ",'", user_info.privileges, "',", // privileges - "null" // extra_info - ");"); + hybridse::sdk::Status status; - ExecuteInsert(nameserver::INTERNAL_DB, sql, &status); + + auto ns_client = cluster_sdk_->GetNsClient(); + + bool ok = ns_client->PutUser("%", name, real_password); + + if (!ok) { + status.code = hybridse::common::StatusCode::kRunError; + status.msg = absl::StrCat("Fail to update user: ", name); + } + return status; } hybridse::sdk::Status SQLClusterRouter::DeleteUser(const std::string& name) { - std::string sql = absl::StrCat("delete from ", nameserver::USER_INFO_NAME, - " where host = '%' and user = '", name, "';"); hybridse::sdk::Status status; - ExecuteSQL(nameserver::INTERNAL_DB, sql, &status); + + auto ns_client = cluster_sdk_->GetNsClient(); + + bool ok = ns_client->DeleteUser("%", name); + + if (!ok) { + status.code = hybridse::common::StatusCode::kRunError; + status.msg = absl::StrCat("Fail to delete user: ", name); + } + return status; } diff --git a/src/tablet/tablet_impl.cc b/src/tablet/tablet_impl.cc index 8c59a4f9184..2f7544f2847 100644 --- a/src/tablet/tablet_impl.cc +++ b/src/tablet/tablet_impl.cc @@ -154,7 +154,8 @@ TabletImpl::TabletImpl() sp_cache_(std::shared_ptr(new SpCache())), notify_path_(), globalvar_changed_notify_path_(), - startup_mode_(::openmldb::type::StartupMode::kStandalone) {} + startup_mode_(::openmldb::type::StartupMode::kStandalone), + user_access_manager_(GetSystemTableIterator()) {} TabletImpl::~TabletImpl() { task_pool_.Stop(true); @@ -5814,6 +5815,17 @@ void TabletImpl::GetAndFlushDeployStats(::google::protobuf::RpcController* contr response->set_code(ReturnCode::kOk); } +bool TabletImpl::IsAuthenticated(const std::string& host, const std::string& username, const std::string& password) { + return user_access_manager_.IsAuthenticated(host, username, password); +} + +void TabletImpl::FlushPrivileges(::google::protobuf::RpcController* controller, + const ::openmldb::api::EmptyRequest* request, + ::openmldb::api::GeneralResponse* response, ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + user_access_manager_.SyncWithDB(); +} + std::function, std::unique_ptr>>(const std::string& table_name)> TabletImpl::GetSystemTableIterator() { diff --git a/src/tablet/tablet_impl.h b/src/tablet/tablet_impl.h index 89ab1c1befa..d299956cb9e 100644 --- a/src/tablet/tablet_impl.h +++ b/src/tablet/tablet_impl.h @@ -26,6 +26,7 @@ #include #include +#include "auth/user_access_manager.h" #include "base/spinlock.h" #include "brpc/server.h" #include "catalog/tablet_catalog.h" @@ -274,11 +275,15 @@ class TabletImpl : public ::openmldb::api::TabletServer { ::openmldb::api::DeployStatsResponse* response, ::google::protobuf::Closure* done) override; + bool IsAuthenticated(const std::string& host, const std::string& username, const std::string& password); + void FlushPrivileges(::google::protobuf::RpcController* controller, const ::openmldb::api::EmptyRequest* request, + ::openmldb::api::GeneralResponse* response, ::google::protobuf::Closure* done); + + private: std::function, std::unique_ptr>>(const std::string& table_name)> GetSystemTableIterator(); - private: class UpdateAggrClosure : public Closure { public: explicit UpdateAggrClosure(const std::function& callback) : callback_(callback) {} @@ -489,6 +494,7 @@ class TabletImpl : public ::openmldb::api::TabletServer { std::unique_ptr deploy_collector_; std::atomic memory_used_ = 0; std::atomic system_memory_usage_rate_ = 0; // [0, 100] + openmldb::auth::UserAccessManager user_access_manager_; }; } // namespace tablet From 59d79f6d116fd3fbb9496d47ca9432375d6a06f0 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Mon, 27 May 2024 14:08:42 +0800 Subject: [PATCH 15/41] feat(parser): simple ANSI SQL rewriter (#3934) * feat(parser): simple ANSI SQL rewriter * feat(draft): translate request mode query * feat: request query rewriter * test: tpc rewrite cases * feat(rewrite): enable ansi sql rewriter in `ExecuteSQL` You may explicitly set this feature on via `set session ansi_sql_rewriter = 'true'` TODO: this rewriter feature should be off by default --- hybridse/src/CMakeLists.txt | 1 + hybridse/src/rewriter/ast_rewriter.cc | 573 +++++++++++++++++++++ hybridse/src/rewriter/ast_rewriter.h | 32 ++ hybridse/src/rewriter/ast_rewriter_test.cc | 237 +++++++++ src/sdk/sql_cluster_router.cc | 39 +- src/sdk/sql_cluster_router.h | 2 + 6 files changed, 883 insertions(+), 1 deletion(-) create mode 100644 hybridse/src/rewriter/ast_rewriter.cc create mode 100644 hybridse/src/rewriter/ast_rewriter.h create mode 100644 hybridse/src/rewriter/ast_rewriter_test.cc diff --git a/hybridse/src/CMakeLists.txt b/hybridse/src/CMakeLists.txt index 80c5cc2a5a3..4f25d87ab70 100644 --- a/hybridse/src/CMakeLists.txt +++ b/hybridse/src/CMakeLists.txt @@ -48,6 +48,7 @@ hybridse_add_src_and_tests(vm) hybridse_add_src_and_tests(codec) hybridse_add_src_and_tests(case) hybridse_add_src_and_tests(passes) +hybridse_add_src_and_tests(rewriter) get_property(SRC_FILE_LIST_STR GLOBAL PROPERTY PROP_SRC_FILE_LIST) string(REPLACE " " ";" SRC_FILE_LIST ${SRC_FILE_LIST_STR}) diff --git a/hybridse/src/rewriter/ast_rewriter.cc b/hybridse/src/rewriter/ast_rewriter.cc new file mode 100644 index 00000000000..9dc90ffdee3 --- /dev/null +++ b/hybridse/src/rewriter/ast_rewriter.cc @@ -0,0 +1,573 @@ +/** + * Copyright (c) 2024 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "rewriter/ast_rewriter.h" + +#include +#include + +#include "absl/cleanup/cleanup.h" +#include "plan/plan_api.h" +#include "zetasql/parser/parse_tree_manual.h" +#include "zetasql/parser/parser.h" +#include "zetasql/parser/unparser.h" + +namespace hybridse { +namespace rewriter { + +// unparser that make some rewrites so outputed SQL is +// compatible with ANSI SQL as much as can +class LastJoinRewriteUnparser : public zetasql::parser::Unparser { + public: + explicit LastJoinRewriteUnparser(std::string* unparsed) : zetasql::parser::Unparser(unparsed) {} + ~LastJoinRewriteUnparser() override {} + LastJoinRewriteUnparser(const LastJoinRewriteUnparser&) = delete; + LastJoinRewriteUnparser& operator=(const LastJoinRewriteUnparser&) = delete; + + void visitASTSelect(const zetasql::ASTSelect* node, void* data) override { + while (true) { + absl::string_view filter_col; + + // 1. filter condition is 'col = 1' + if (node->where_clause() != nullptr && + node->where_clause()->expression()->node_kind() == zetasql::AST_BINARY_EXPRESSION) { + auto expr = node->where_clause()->expression()->GetAsOrNull(); + if (expr && expr->op() == zetasql::ASTBinaryExpression::Op::EQ && !expr->is_not()) { + { + auto lval = expr->lhs()->GetAsOrNull(); + auto rval = expr->rhs()->GetAsOrNull(); + if (lval && rval && lval->image() == "1") { + // TODO(someone): + // 1. consider lval->iamge() as '1L' + // 2. consider rval as . + filter_col = rval->last_name()->GetAsStringView(); + } + } + if (filter_col.empty()) { + auto lval = expr->rhs()->GetAsOrNull(); + auto rval = expr->lhs()->GetAsOrNull(); + if (lval && rval && lval->image() == "1") { + // TODO(someone): + // 1. consider lval->iamge() as '1L' + // 2. consider rval as . + filter_col = rval->last_name()->GetAsStringView(); + } + } + } + } + + // 2. FROM a subquery: SELECT ... t1 LEFT JOIN t2 WINDOW + const zetasql::ASTPathExpression* join_lhs_key = nullptr; + const zetasql::ASTPathExpression* join_rhs_key = nullptr; + if (node->from_clause() == nullptr) { + break; + } + auto sub = node->from_clause()->table_expression()->GetAsOrNull(); + if (!sub) { + break; + } + auto subquery = sub->subquery(); + if (subquery->with_clause() != nullptr || subquery->order_by() != nullptr || + subquery->limit_offset() != nullptr) { + break; + } + + auto inner_select = subquery->query_expr()->GetAsOrNull(); + if (!inner_select) { + break; + } + // select have window + if (inner_select->window_clause() == nullptr || inner_select->from_clause() == nullptr) { + break; + } + + // 3. CHECK FROM CLAUSE: must 't1 LEFT JOIN t2 on t1.key = t2.key' + if (!inner_select->from_clause()) { + break; + } + auto join = inner_select->from_clause()->table_expression()->GetAsOrNull(); + if (join == nullptr || join->join_type() != zetasql::ASTJoin::LEFT || join->on_clause() == nullptr) { + break; + } + auto on_expr = join->on_clause()->expression()->GetAsOrNull(); + if (on_expr == nullptr || on_expr->is_not() || on_expr->op() != zetasql::ASTBinaryExpression::EQ) { + break; + } + + // still might null + join_lhs_key = on_expr->lhs()->GetAsOrNull(); + join_rhs_key = on_expr->rhs()->GetAsOrNull(); + if (join_lhs_key == nullptr || join_rhs_key == nullptr) { + break; + } + + // 3. CHECK row_id is row_number() over w FROM select_list + bool found = false; + absl::string_view window_name; + for (auto col : inner_select->select_list()->columns()) { + if (col->alias() && col->alias()->GetAsStringView() == filter_col) { + auto agg_func = col->expression()->GetAsOrNull(); + if (!agg_func || !agg_func->function()) { + break; + } + + auto w = agg_func->window_spec(); + if (!w || w->base_window_name() == nullptr) { + break; + } + window_name = w->base_window_name()->GetAsStringView(); + + auto ph = agg_func->function()->function(); + if (ph->num_names() == 1 && + absl::AsciiStrToLower(ph->first_name()->GetAsStringView()) == "row_number") { + opt_out_row_number_col_ = col; + found = true; + break; + } + } + } + if (!found || window_name.empty()) { + break; + } + + // 4. CHECK WINDOW CLAUSE + { + if (inner_select->window_clause()->windows().size() != 1) { + // targeting single window only + break; + } + auto win = inner_select->window_clause()->windows().front(); + if (win->name()->GetAsStringView() != window_name) { + break; + } + auto spec = win->window_spec(); + if (spec->window_frame() != nullptr || spec->partition_by() == nullptr || spec->order_by() == nullptr) { + // TODO(someone): allow unbounded window frame + break; + } + + // PARTITION BY contains join_lhs_key + // ORDER BY is join_rhs_key + bool partition_meet = false; + for (auto expr : spec->partition_by()->partitioning_expressions()) { + auto e = expr->GetAsOrNull(); + if (e) { + if (e->last_name()->GetAsStringView() == join_lhs_key->last_name()->GetAsStringView()) { + partition_meet = true; + } + } + } + + if (!partition_meet) { + break; + } + + if (spec->order_by()->ordering_expressions().size() != 1) { + break; + } + + if (spec->order_by()->ordering_expressions().front()->ordering_spec() != + zetasql::ASTOrderingExpression::DESC) { + break; + } + + auto e = spec->order_by() + ->ordering_expressions() + .front() + ->expression() + ->GetAsOrNull(); + if (!e) { + break; + } + + // rewrite + { + opt_out_window_ = inner_select->window_clause(); + opt_out_where_ = node->where_clause(); + opt_join_ = join; + opt_in_last_join_order_by_ = e; + absl::Cleanup clean = [&]() { + opt_out_window_ = nullptr; + opt_out_where_ = nullptr; + opt_out_row_number_col_ = nullptr; + opt_join_ = nullptr; + }; + + // inline zetasql::parser::Unparser::visitASTSelect(node, data); + { + PrintOpenParenIfNeeded(node); + println(); + print("SELECT"); + if (node->hint() != nullptr) { + node->hint()->Accept(this, data); + } + if (node->anonymization_options() != nullptr) { + print("WITH ANONYMIZATION OPTIONS"); + node->anonymization_options()->Accept(this, data); + } + if (node->distinct()) { + print("DISTINCT"); + } + + // Visit all children except hint() and anonymization_options, which we + // processed above. We can't just use visitASTChildren(node, data) because + // we need to insert the DISTINCT modifier after the hint and anonymization + // nodes and before everything else. + for (int i = 0; i < node->num_children(); ++i) { + const zetasql::ASTNode* child = node->child(i); + if (child != node->hint() && child != node->anonymization_options()) { + child->Accept(this, data); + } + } + + println(); + PrintCloseParenIfNeeded(node); + } + + return; + } + } + + break; + } + + zetasql::parser::Unparser::visitASTSelect(node, data); + } + + void visitASTJoin(const zetasql::ASTJoin* node, void* data) override { + if (opt_join_ && opt_join_ == node) { + node->child(0)->Accept(this, data); + + if (node->join_type() == zetasql::ASTJoin::COMMA) { + print(","); + } else { + println(); + if (node->natural()) { + print("NATURAL"); + } + print("LAST"); + print(node->GetSQLForJoinHint()); + + print("JOIN"); + } + println(); + + // This will print hints, the rhs, and the ON or USING clause. + for (int i = 1; i < node->num_children(); i++) { + node->child(i)->Accept(this, data); + if (opt_in_last_join_order_by_ && node->child(i)->IsTableExpression()) { + print("ORDER BY"); + opt_in_last_join_order_by_->Accept(this, data); + } + } + + return; + } + + zetasql::parser::Unparser::visitASTJoin(node, data); + } + + void visitASTSelectList(const zetasql::ASTSelectList* node, void* data) override { + println(); + { + for (int i = 0; i < node->num_children(); i++) { + if (opt_out_row_number_col_ && node->columns(i) == opt_out_row_number_col_) { + continue; + } + if (i > 0) { + println(","); + } + node->child(i)->Accept(this, data); + } + } + } + + void visitASTWindowClause(const zetasql::ASTWindowClause* node, void* data) override { + if (opt_out_window_ && opt_out_window_ == node) { + return; + } + + zetasql::parser::Unparser::visitASTWindowClause(node, data); + } + + void visitASTWhereClause(const zetasql::ASTWhereClause* node, void* data) override { + if (opt_out_where_ && opt_out_where_ == node) { + return; + } + zetasql::parser::Unparser::visitASTWhereClause(node, data); + } + + private: + const zetasql::ASTWindowClause* opt_out_window_ = nullptr; + const zetasql::ASTWhereClause* opt_out_where_ = nullptr; + const zetasql::ASTSelectColumn* opt_out_row_number_col_ = nullptr; + const zetasql::ASTJoin* opt_join_ = nullptr; + const zetasql::ASTPathExpression* opt_in_last_join_order_by_ = nullptr; +}; + +// SELECT: +// WHERE col = 0 +// FROM (subquery): +// subquery is UNION ALL, or contains left-most query is UNION ALL +// and UNION ALL is select const ..., 0 as col UNION ALL (select .., 1 as col table) +class RequestQueryRewriteUnparser : public zetasql::parser::Unparser { + public: + explicit RequestQueryRewriteUnparser(std::string* unparsed) : zetasql::parser::Unparser(unparsed) {} + ~RequestQueryRewriteUnparser() override {} + RequestQueryRewriteUnparser(const RequestQueryRewriteUnparser&) = delete; + RequestQueryRewriteUnparser& operator=(const RequestQueryRewriteUnparser&) = delete; + + void visitASTSelect(const zetasql::ASTSelect* node, void* data) override { + while (true) { + if (outer_most_select_ != nullptr) { + break; + } + + outer_most_select_ = node; + if (node->where_clause() == nullptr) { + break; + } + absl::string_view filter_col; + const zetasql::ASTExpression* filter_expr; + + // 1. filter condition is 'col = 0' + if (node->where_clause()->expression()->node_kind() != zetasql::AST_BINARY_EXPRESSION) { + break; + } + auto expr = node->where_clause()->expression()->GetAsOrNull(); + if (!expr || expr->op() != zetasql::ASTBinaryExpression::Op::EQ || expr->is_not()) { + break; + } + { + auto rval = expr->rhs()->GetAsOrNull(); + if (rval) { + // TODO(someone): + // 2. consider rval as . + filter_col = rval->last_name()->GetAsStringView(); + filter_expr = expr->lhs(); + } + } + if (filter_col.empty()) { + auto rval = expr->lhs()->GetAsOrNull(); + if (rval) { + // TODO(someone): + // 2. consider rval as . + filter_col = rval->last_name()->GetAsStringView(); + filter_expr = expr->rhs(); + } + } + if (filter_col.empty() || !filter_expr) { + break; + } + + if (node->from_clause() == nullptr) { + break; + } + auto sub = node->from_clause()->table_expression()->GetAsOrNull(); + if (!sub) { + break; + } + auto subquery = sub->subquery(); + + findUnionAllForQuery(subquery, filter_col, filter_expr, node->where_clause()); + + break; // fallback normal + } + + zetasql::parser::Unparser::visitASTSelect(node, data); + } + + void visitASTSetOperation(const zetasql::ASTSetOperation* node, void* data) override { + if (node == detected_request_block_) { + node->inputs().back()->Accept(this, data); + } else { + zetasql::parser::Unparser::visitASTSetOperation(node, data); + } + } + + void visitASTQueryStatement(const zetasql::ASTQueryStatement* node, void* data) override { + node->query()->Accept(this, data); + if (!list_.empty() && !node->config_clause()) { + constSelectListAsConfigClause(list_, data); + } else { + if (node->config_clause() != nullptr) { + println(); + node->config_clause()->Accept(this, data); + } + } + } + + void visitASTWhereClause(const zetasql::ASTWhereClause* node, void* data) override { + if (node != filter_clause_) { + zetasql::parser::Unparser::visitASTWhereClause(node, data); + } + } + + private: + void findUnionAllForQuery(const zetasql::ASTQuery* query, absl::string_view label_name, + const zetasql::ASTExpression* filter_expr, const zetasql::ASTWhereClause* filter) { + if (!query) { + return; + } + auto qe = query->query_expr(); + switch (qe->node_kind()) { + case zetasql::AST_SET_OPERATION: { + auto set = qe->GetAsOrNull(); + if (set && set->op_type() == zetasql::ASTSetOperation::UNION && set->distinct() == false && + set->hint() == nullptr && set->inputs().size() == 2) { + [[maybe_unused]] bool ret = + findUnionAllInput(set->inputs().at(0), set->inputs().at(1), label_name, filter_expr, filter) || + findUnionAllInput(set->inputs().at(0), set->inputs().at(1), label_name, filter_expr, filter); + if (ret) { + detected_request_block_ = set; + } + } + break; + } + case zetasql::AST_QUERY: { + findUnionAllForQuery(qe->GetAsOrNull(), label_name, filter_expr, filter); + break; + } + case zetasql::AST_SELECT: { + auto select = qe->GetAsOrNull(); + if (select->from_clause() && + select->from_clause()->table_expression()->node_kind() == zetasql::AST_TABLE_SUBQUERY) { + auto sub = select->from_clause()->table_expression()->GetAsOrNull(); + if (sub && sub->subquery()) { + findUnionAllForQuery(sub->subquery(), label_name, filter_expr, filter); + } + } + break; + } + default: + break; + } + } + + void constSelectListAsConfigClause(const std::vector& selects, void* data) { + print("CONFIG (execute_mode = 'request', values = ("); + for (int i = 0; i < selects.size(); ++i) { + selects.at(i)->Accept(this, data); + if (i + 1 < selects.size()) { + print(","); + } + } + print(") )"); + } + + bool findUnionAllInput(const zetasql::ASTQueryExpression* lhs, const zetasql::ASTQueryExpression* rhs, + absl::string_view label_name, const zetasql::ASTExpression* filter_expr, + const zetasql::ASTWhereClause* filter) { + // lhs is select const + label_name of value 0 + auto lselect = lhs->GetAsOrNull(); + if (!lselect || lselect->num_children() > 1) { + // only select_list required, otherwise size > 1 + return false; + } + + bool has_label_col_0 = false; + const zetasql::ASTExpression* label_expr_0 = nullptr; + std::vector vec; + for (auto col : lselect->select_list()->columns()) { + if (col->alias() && col->alias()->GetAsStringView() == label_name) { + has_label_col_0 = true; + label_expr_0 = col->expression(); + } else { + vec.push_back(col->expression()); + } + } + + // rhs is simple selects from table + label_name of value 1 + auto rselect = rhs->GetAsOrNull(); + if (!rselect || rselect->num_children() > 2 || !rselect->from_clause()) { + // only select_list + from_clause required + return false; + } + if (rselect->from_clause()->table_expression()->node_kind() != zetasql::AST_TABLE_PATH_EXPRESSION) { + return false; + } + + bool has_label_col_1 = false; + const zetasql::ASTExpression* label_expr_1 = nullptr; + for (auto col : rselect->select_list()->columns()) { + if (col->alias() && col->alias()->GetAsStringView() == label_name) { + has_label_col_1 = true; + label_expr_1 = col->expression(); + } + } + + LOG(INFO) << "label expr 0: " << label_expr_0->SingleNodeDebugString(); + LOG(INFO) << "label expr 1: " << label_expr_1->SingleNodeDebugString(); + LOG(INFO) << "filter expr: " << filter_expr->SingleNodeDebugString(); + + if (has_label_col_0 && has_label_col_1 && + label_expr_0->SingleNodeDebugString() != label_expr_1->SingleNodeDebugString() && + label_expr_0->SingleNodeDebugString() == filter_expr->SingleNodeDebugString()) { + list_ = vec; + filter_clause_ = filter; + return true; + } + + return false; + } + + private: + const zetasql::ASTSelect* outer_most_select_ = nullptr; + // detected request query block, set by when visiting outer most query + const zetasql::ASTSetOperation* detected_request_block_ = nullptr; + const zetasql::ASTWhereClause* filter_clause_; + + std::vector list_; +}; + +absl::StatusOr Rewrite(absl::string_view query) { + auto str = std::string(query); + { + std::unique_ptr ast; + auto s = hybridse::plan::ParseStatement(str, &ast); + if (!s.ok()) { + return s; + } + + if (ast->statement() && ast->statement()->node_kind() == zetasql::AST_QUERY_STATEMENT) { + std::string unparsed_; + LastJoinRewriteUnparser unparser(&unparsed_); + ast->statement()->Accept(&unparser, nullptr); + unparser.FlushLine(); + str = unparsed_; + } + } + { + std::unique_ptr ast; + auto s = hybridse::plan::ParseStatement(str, &ast); + if (!s.ok()) { + return s; + } + + if (ast->statement() && ast->statement()->node_kind() == zetasql::AST_QUERY_STATEMENT) { + std::string unparsed_; + RequestQueryRewriteUnparser unparser(&unparsed_); + ast->statement()->Accept(&unparser, nullptr); + unparser.FlushLine(); + str = unparsed_; + } + } + + return str; +} + +} // namespace rewriter +} // namespace hybridse diff --git a/hybridse/src/rewriter/ast_rewriter.h b/hybridse/src/rewriter/ast_rewriter.h new file mode 100644 index 00000000000..17ea7ad0d04 --- /dev/null +++ b/hybridse/src/rewriter/ast_rewriter.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef HYBRIDSE_SRC_REWRITER_AST_REWRITER_H_ +#define HYBRIDSE_SRC_REWRITER_AST_REWRITER_H_ + +#include + +#include "absl/status/statusor.h" + +namespace hybridse { +namespace rewriter { + +absl::StatusOr Rewrite(absl::string_view query); + +} // namespace rewriter +} // namespace hybridse + +#endif // HYBRIDSE_SRC_REWRITER_AST_REWRITER_H_ diff --git a/hybridse/src/rewriter/ast_rewriter_test.cc b/hybridse/src/rewriter/ast_rewriter_test.cc new file mode 100644 index 00000000000..7585ada71a6 --- /dev/null +++ b/hybridse/src/rewriter/ast_rewriter_test.cc @@ -0,0 +1,237 @@ +/** + * Copyright 2024 OpenMLDB authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "rewriter/ast_rewriter.h" + +#include +#include + +#include "absl/strings/ascii.h" +#include "gtest/gtest.h" +#include "plan/plan_api.h" +#include "zetasql/parser/parser.h" + +namespace hybridse { +namespace rewriter { + +struct Case { + absl::string_view in; + absl::string_view out; +}; + +class ASTRewriterTest : public ::testing::TestWithParam {}; + +std::vector strip_cases = { + // eliminate LEFT JOIN WINDOW -> LAST JOIN + {R"s( + SELECT id, val, k, ts, idr, valr FROM ( + SELECT t1.*, t2.id as idr, t2.val as valr, row_number() over w as any_id + FROM t1 LEFT JOIN t2 ON t1.k = t2.k + WINDOW w as (PARTITION BY t1.id,t1.k order by t2.ts desc) + ) t WHERE any_id = 1)s", + R"e( +SELECT + id, + val, + k, + ts, + idr, + valr +FROM + ( + SELECT + t1.*, + t2.id AS idr, + t2.val AS valr + FROM + t1 + LAST JOIN + t2 + ORDER BY t2.ts + ON t1.k = t2.k + ) AS t +)e"}, + {R"( +SELECT id, k, agg +FROM ( + SELECT id, k, label, count(val) over w as agg + FROM ( + SELECT 6 as id, "xxx" as val, 10 as k, 9000 as ts, 0 as label + UNION ALL + SELECT *, 1 as label FROM t1 + ) t + WINDOW w as (PARTITION BY k ORDER BY ts rows between unbounded preceding and current row) +) t WHERE label = 0)", + R"( +SELECT + id, + k, + agg +FROM + ( + SELECT + id, + k, + label, + count(val) OVER (w) AS agg + FROM + ( + SELECT + *, + 1 AS label + FROM + t1 + ) AS t + WINDOW w AS (PARTITION BY k + ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) + ) AS t +CONFIG (execute_mode = 'request', values = (6, "xxx", 10, 9000) ) +)"}, + // simplist request query + {R"s( + SELECT id, k + FROM ( + SELECT 6 as id, "xxx" as val, 10 as k, 9000 as ts, 0 as label + UNION ALL + SELECT *, 1 as label FROM t1 + ) t WHERE label = 0)s", + R"s(SELECT + id, + k +FROM + ( + SELECT + *, + 1 AS label + FROM + t1 + ) AS t +CONFIG (execute_mode = 'request', values = (6, "xxx", 10, 9000) ) +)s"}, + + // TPC-C case + {R"(SELECT C_ID, C_CITY, C_STATE, C_CREDIT, C_CREDIT_LIM, C_BALANCE, C_PAYMENT_CNT, C_DELIVERY_CNT + FROM ( + SELECT C_ID, C_CITY, C_STATE, C_CREDIT, C_CREDIT_LIM, C_BALANCE, C_PAYMENT_CNT, C_DELIVERY_CNT, label FROM ( + SELECT 1 AS C_ID, 1 AS C_D_ID, 1 AS C_W_ID, "John" AS C_FIRST, "M" AS C_MIDDLE, "Smith" AS C_LAST, "123 Main St" AS C_STREET_1, "Apt 101" AS C_STREET_2, "Springfield" AS C_CITY, "IL" AS C_STATE, 12345 AS C_ZIP, "555-123-4567" AS C_PHONE, timestamp("2024-01-01 00:00:00") AS C_SINCE, "BC" AS C_CREDIT, 10000.0 AS C_CREDIT_LIM, 0.5 AS C_DISCOUNT, 5000.0 AS C_BALANCE, 0.0 AS C_YTD_PAYMENT, 0 AS C_PAYMENT_CNT, 0 AS C_DELIVERY_CNT, "Additional customer data..." AS C_DATA, 0 as label + UNION ALL + SELECT *, 1 as label FROM CUSTOMER + ) t + ) t WHERE label = 0)", + R"s( +SELECT + C_ID, + C_CITY, + C_STATE, + C_CREDIT, + C_CREDIT_LIM, + C_BALANCE, + C_PAYMENT_CNT, + C_DELIVERY_CNT +FROM + ( + SELECT + C_ID, + C_CITY, + C_STATE, + C_CREDIT, + C_CREDIT_LIM, + C_BALANCE, + C_PAYMENT_CNT, + C_DELIVERY_CNT, + label + FROM + ( + SELECT + *, + 1 AS label + FROM + CUSTOMER + ) AS t + ) AS t +CONFIG (execute_mode = 'request', values = (1, 1, 1, "John", "M", "Smith", "123 Main St", "Apt 101", +"Springfield", "IL", 12345, "555-123-4567", timestamp("2024-01-01 00:00:00"), "BC", 10000.0, 0.5, 5000.0, +0.0, 0, 0, "Additional customer data...") ) + )s"}, + + {R"( +SELECT C_ID, C_CITY, C_STATE, C_CREDIT, C_CREDIT_LIM, C_BALANCE, C_PAYMENT_CNT, C_DELIVERY_CNT + FROM ( + SELECT C_ID, C_CITY, C_STATE, C_CREDIT, C_CREDIT_LIM, C_BALANCE, C_PAYMENT_CNT, C_DELIVERY_CNT, label FROM ( + SELECT 1 AS C_ID, 1 AS C_D_ID, 1 AS C_W_ID, "John" AS C_FIRST, "M" AS C_MIDDLE, "Smith" AS C_LAST, "123 Main St" AS C_STREET_1, "Apt 101" AS C_STREET_2, "Springfield" AS C_CITY, "IL" AS C_STATE, 12345 AS C_ZIP, "555-123-4567" AS C_PHONE, timestamp("2024-01-01 00:00:00") AS C_SINCE, "BC" AS C_CREDIT, 10000.0 AS C_CREDIT_LIM, 0.5 AS C_DISCOUNT, 9000.0 AS C_BALANCE, 0.0 AS C_YTD_PAYMENT, 0 AS C_PAYMENT_CNT, 0 AS C_DELIVERY_CNT, "Additional customer data..." AS C_DATA, 0 as label + UNION ALL + SELECT *, 1 as label FROM CUSTOMER + ) t + ) t WHERE label = 0)", + R"( +SELECT + C_ID, + C_CITY, + C_STATE, + C_CREDIT, + C_CREDIT_LIM, + C_BALANCE, + C_PAYMENT_CNT, + C_DELIVERY_CNT +FROM + ( + SELECT + C_ID, + C_CITY, + C_STATE, + C_CREDIT, + C_CREDIT_LIM, + C_BALANCE, + C_PAYMENT_CNT, + C_DELIVERY_CNT, + label + FROM + ( + SELECT + *, + 1 AS label + FROM + CUSTOMER + ) AS t + ) AS t +CONFIG (execute_mode = 'request', values = (1, 1, 1, "John", "M", "Smith", "123 Main St", "Apt 101", +"Springfield", "IL", 12345, "555-123-4567", timestamp("2024-01-01 00:00:00"), "BC", 10000.0, 0.5, 9000.0, +0.0, 0, 0, "Additional customer data...") ) +)"}, +}; + +INSTANTIATE_TEST_SUITE_P(Rules, ASTRewriterTest, ::testing::ValuesIn(strip_cases)); + +TEST_P(ASTRewriterTest, Correctness) { + auto& c = GetParam(); + + auto s = hybridse::rewriter::Rewrite(c.in); + ASSERT_TRUE(s.ok()) << s.status(); + + ASSERT_EQ(absl::StripAsciiWhitespace(c.out), absl::StripAsciiWhitespace(s.value())); + + std::unique_ptr out; + auto ss = ::hybridse::plan::ParseStatement(s.value(), &out); + ASSERT_TRUE(ss.ok()) << ss; +} + +} // namespace rewriter +} // namespace hybridse + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/sdk/sql_cluster_router.cc b/src/sdk/sql_cluster_router.cc index e58eb8cd2cc..068538ba5e0 100644 --- a/src/sdk/sql_cluster_router.cc +++ b/src/sdk/sql_cluster_router.cc @@ -50,6 +50,7 @@ #include "plan/plan_api.h" #include "proto/fe_common.pb.h" #include "proto/tablet.pb.h" +#include "rewriter/ast_rewriter.h" #include "rpc/rpc_client.h" #include "schema/schema_adapter.h" #include "sdk/base.h" @@ -2676,14 +2677,34 @@ std::shared_ptr SQLClusterRouter::ExecuteSQL(const std } std::shared_ptr SQLClusterRouter::ExecuteSQL( - const std::string& db, const std::string& sql, std::shared_ptr parameter, + const std::string& db, const std::string& str, std::shared_ptr parameter, bool is_online_mode, bool is_sync_job, int offline_job_timeout, hybridse::sdk::Status* status) { RET_IF_NULL_AND_WARN(status, "output status is nullptr"); // functions we called later may not change the status if it's succeed. So if we pass error status here, we'll get a // fake error status->SetOK(); + + std::string sql = str; hybridse::vm::SqlContext ctx; + if (ANSISQLRewriterEnabled()) { + // If true, enable the ANSI SQL rewriter that would rewrite some SQL query + // for pre-defined pattern to OpenMLDB SQL extensions. Rewrite phase is before general SQL compilation. + // + // OpenMLDB SQL extensions, such as request mode query or LAST JOIN, would be helpful + // to simplify those that comes from like SparkSQL, and reserve the same semantics meaning. + // + // Rewrite rules are based on ASTNode, possibly lack some semantic checks. Turn it off if things + // go abnormal during rewrite phase. + auto s = hybridse::rewriter::Rewrite(sql); + if (s.ok()) { + LOG(INFO) << "rewrited: " << s.value(); + sql = s.value(); + } else { + LOG(WARNING) << s.status(); + } + } ctx.sql = sql; + auto sql_status = hybridse::plan::PlanAPI::CreatePlanTreeFromScript(&ctx); if (!sql_status.isOK()) { COPY_PREPEND_AND_WARN(status, sql_status, "create logic plan tree failed"); @@ -3196,6 +3217,18 @@ bool SQLClusterRouter::IsSyncJob() { return false; } +bool SQLClusterRouter::ANSISQLRewriterEnabled() { + // TODO(xxx): mark fn const + + std::lock_guard<::openmldb::base::SpinMutex> lock(mu_); + auto it = session_variables_.find("ansi_sql_rewriter"); + if (it != session_variables_.end() && it->second == "false") { + return false; + } + // TODO(xxx): always disable by default + return true; +} + int SQLClusterRouter::GetJobTimeout() { std::lock_guard<::openmldb::base::SpinMutex> lock(mu_); auto it = session_variables_.find("job_timeout"); @@ -3267,6 +3300,10 @@ ::hybridse::sdk::Status SQLClusterRouter::SetVariable(hybridse::node::SetPlanNod return {StatusCode::kCmdError, "Fail to parse spark config, set like 'spark.executor.memory=2g;spark.executor.cores=2'"}; } + } else if (key == "ansi_sql_rewriter") { + if (value != "true" && value != "false") { + return {StatusCode::kCmdError, "the value of " + key + " must be true|false"}; + } } else { return {}; } diff --git a/src/sdk/sql_cluster_router.h b/src/sdk/sql_cluster_router.h index 3d13cafa240..e917c170a14 100644 --- a/src/sdk/sql_cluster_router.h +++ b/src/sdk/sql_cluster_router.h @@ -443,6 +443,8 @@ class SQLClusterRouter : public SQLRouter { const base::Slice& value, const std::vector>& tablets); + bool ANSISQLRewriterEnabled(); + private: std::shared_ptr options_; std::string db_; From e307fd939b5dc7346964fd10358f8b2b29417750 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 13:20:08 +0800 Subject: [PATCH 16/41] build(deps-dev): bump urllib3 from 1.26.18 to 1.26.19 in /docs (#3948) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.18 to 1.26.19. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/1.26.19/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.18...1.26.19) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/poetry.lock b/docs/poetry.lock index ca6c9ccb8c6..39577275304 100644 --- a/docs/poetry.lock +++ b/docs/poetry.lock @@ -670,13 +670,13 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "urllib3" -version = "1.26.18" +version = "1.26.19" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, - {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, + {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, + {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, ] [package.extras] From 818d292cc5190c65ef3ead678f6742e828d0f163 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Wed, 26 Jun 2024 12:34:11 +0800 Subject: [PATCH 17/41] feat(udf): isin (#3939) --- cases/query/udf_query.yaml | 19 ++++++++++ hybridse/src/udf/default_defs/array_def.cc | 41 ++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/cases/query/udf_query.yaml b/cases/query/udf_query.yaml index fefe1380dbb..ee9cad2d667 100644 --- a/cases/query/udf_query.yaml +++ b/cases/query/udf_query.yaml @@ -536,6 +536,25 @@ cases: data: | true, true, false, false, true, false, true, false, true, false, true + - id: isin + mode: request-unsupport + inputs: + - name: t1 + columns: ["col1:int32", "std_ts:timestamp", "col2:string"] + indexs: ["index1:col1:std_ts"] + rows: + - [1, 1590115420001, "ABCabcabc"] + sql: | + select + isin(2, [2,2]) as c0, + isin(cast(3 as int64), ARRAY[NULL, 1, 2]) as c1 + expect: + columns: + - c0 bool + - c1 bool + data: | + true, false + - id: array_split mode: request-unsupport inputs: diff --git a/hybridse/src/udf/default_defs/array_def.cc b/hybridse/src/udf/default_defs/array_def.cc index a1f35f38e35..b5c36bc3d7e 100644 --- a/hybridse/src/udf/default_defs/array_def.cc +++ b/hybridse/src/udf/default_defs/array_def.cc @@ -37,8 +37,30 @@ struct ArrayContains { // - bool/intxx/float/double -> bool/intxx/float/double // - Timestamp/Date/StringRef -> Timestamp*/Date*/StringRef* bool operator()(ArrayRef* arr, ParamType v, bool is_null) { - // NOTE: array_contains([null], null) returns null - // this might not expected + for (uint64_t i = 0; i < arr->size; ++i) { + if constexpr (std::is_pointer_v) { + // null or same value returns true + if ((is_null && arr->nullables[i]) || (!arr->nullables[i] && *arr->raw[i] == *v)) { + return true; + } + } else { + if ((is_null && arr->nullables[i]) || (!arr->nullables[i] && arr->raw[i] == v)) { + return true; + } + } + } + return false; + } +}; + +template +struct IsIn { + // udf registry types + using Args = std::tuple, ArrayRef>; + + using ParamType = typename DataTypeTrait::CCallArgType; + + bool operator()(ParamType v, bool is_null, ArrayRef* arr) { for (uint64_t i = 0; i < arr->size; ++i) { if constexpr (std::is_pointer_v) { // null or same value returns true @@ -98,6 +120,21 @@ void DefaultUdfLibrary::InitArrayUdfs() { @since 0.7.0 )"); + RegisterExternalTemplate("isin") + .args_in() + .doc(R"( + @brief isin(value, array) - Returns true if the array contains the value. + + Example: + + @code{.sql} + select isin(2, [2,2]) as c0; + -- output true + @endcode + + @since 0.9.1 + )"); + RegisterExternal("split_array") .returns>() .return_by_arg(true) From 6b06e38a7c8e4f794503afb8b8ccfab8a1b96297 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Wed, 26 Jun 2024 12:52:41 +0800 Subject: [PATCH 18/41] feat(#3916): support @@execute_mode = 'request' (#3924) --- hybridse/include/vm/engine.h | 4 ++ hybridse/src/vm/engine.cc | 45 +++++++++++++++++------ hybridse/src/vm/engine_context.cc | 3 +- src/client/tablet_client.cc | 3 +- src/client/tablet_client.h | 2 +- src/sdk/internal/system_variable.cc | 57 +++++++++++++++++++++++++++++ src/sdk/internal/system_variable.h | 40 ++++++++++++++++++++ src/sdk/sql_cluster_router.cc | 37 ++++++++++++++----- src/sdk/sql_cluster_router.h | 5 ++- src/sdk/sql_router.h | 2 +- src/sdk/sql_router_sdk.i | 1 + src/tablet/tablet_impl.cc | 8 +++- 12 files changed, 178 insertions(+), 29 deletions(-) create mode 100644 src/sdk/internal/system_variable.cc create mode 100644 src/sdk/internal/system_variable.h diff --git a/hybridse/include/vm/engine.h b/hybridse/include/vm/engine.h index 09586a8b03d..7e183d43c33 100644 --- a/hybridse/include/vm/engine.h +++ b/hybridse/include/vm/engine.h @@ -429,6 +429,10 @@ class Engine { /// request row info exists in 'values' option, as a format of: /// 1. [(col1_expr, col2_expr, ... ), (...), ...] /// 2. (col1_expr, col2_expr, ... ) + // + // This function only check on request/batchrequest mode, for batch mode it does nothing. + // As for old-fashioned usage, request row does not need to appear in SQL, so it won't report + // error even request rows is empty, instead checks should performed at the very beginning of Compute. static absl::Status ExtractRequestRowsInSQL(SqlContext* ctx); std::shared_ptr GetCacheLocked(const std::string& db, diff --git a/hybridse/src/vm/engine.cc b/hybridse/src/vm/engine.cc index 5c179fb79b6..a2aaa30f305 100644 --- a/hybridse/src/vm/engine.cc +++ b/hybridse/src/vm/engine.cc @@ -462,17 +462,27 @@ int32_t RequestRunSession::Run(const uint32_t task_id, const Row& in_row, Row* o if (!sql_request_rows.empty()) { row = sql_request_rows.at(0); } - auto task = std::dynamic_pointer_cast(compile_info_) - ->get_sql_context() - .cluster_job->GetTask(task_id) - .GetRoot(); + + auto info = std::dynamic_pointer_cast(compile_info_); + auto main_task_id = info->GetClusterJob()->main_task_id(); + if (task_id == main_task_id && !info->GetRequestSchema().empty() && row.empty()) { + // a non-empty request row required but it not. + // checks only happen for a top level query, not subquery, + // since query internally may construct a empty row as input row, + // not meaning row with no columns, but row with all column values NULL + LOG(WARNING) << "request SQL requires a non-empty request row, but empty row received"; + // TODO(someone): use status + return common::StatusCode::kRunSessionError; + } + + auto task = info->get_sql_context().cluster_job->GetTask(task_id).GetRoot(); + if (nullptr == task) { LOG(WARNING) << "fail to run request plan: taskid" << task_id << " not exist!"; return -2; } DLOG(INFO) << "Request Row Run with task_id " << task_id; - RunnerContext ctx(std::dynamic_pointer_cast(compile_info_)->get_sql_context().cluster_job, row, - sp_name_, is_debug_); + RunnerContext ctx(info->get_sql_context().cluster_job, row, sp_name_, is_debug_); auto output = task->RunWithCache(ctx); if (!output) { LOG(WARNING) << "Run request plan output is null"; @@ -491,13 +501,24 @@ int32_t BatchRequestRunSession::Run(const std::vector& request_batch, std:: } int32_t BatchRequestRunSession::Run(const uint32_t id, const std::vector& request_batch, std::vector& output) { - std::vector<::hybridse::codec::Row>& sql_request_rows = - std::dynamic_pointer_cast(GetCompileInfo())->get_sql_context().request_rows; + auto info = std::dynamic_pointer_cast(GetCompileInfo()); + std::vector<::hybridse::codec::Row>& sql_request_rows = info->get_sql_context().request_rows; + + std::vector<::hybridse::codec::Row> rows = sql_request_rows; + if (rows.empty()) { + rows = request_batch; + } + + auto main_task_id = info->GetClusterJob()->main_task_id(); + if (id != main_task_id && !info->GetRequestSchema().empty() && rows.empty()) { + // a non-empty request row list required but it not + LOG(WARNING) << "batchrequest SQL requires a non-empty request row list, but empty row list received"; + // TODO(someone): use status + return common::StatusCode::kRunSessionError; + } - RunnerContext ctx(std::dynamic_pointer_cast(compile_info_)->get_sql_context().cluster_job, - sql_request_rows.empty() ? request_batch : sql_request_rows, sp_name_, is_debug_); - auto task = - std::dynamic_pointer_cast(compile_info_)->get_sql_context().cluster_job->GetTask(id).GetRoot(); + RunnerContext ctx(info->get_sql_context().cluster_job, rows, sp_name_, is_debug_); + auto task = info->get_sql_context().cluster_job->GetTask(id).GetRoot(); if (nullptr == task) { LOG(WARNING) << "Fail to run request plan: taskid" << id << " not exist!"; return -2; diff --git a/hybridse/src/vm/engine_context.cc b/hybridse/src/vm/engine_context.cc index 570726aa0eb..47ff39437d6 100644 --- a/hybridse/src/vm/engine_context.cc +++ b/hybridse/src/vm/engine_context.cc @@ -17,6 +17,7 @@ #include "vm/engine_context.h" #include "absl/container/flat_hash_map.h" +#include "absl/strings/ascii.h" namespace hybridse { namespace vm { @@ -61,7 +62,7 @@ std::string EngineModeName(EngineMode mode) { absl::StatusOr UnparseEngineMode(absl::string_view str) { auto& m = getModeMap(); - auto it = m.find(str); + auto it = m.find(absl::AsciiStrToLower(str)); if (it != m.end()) { return it->second; } diff --git a/src/client/tablet_client.cc b/src/client/tablet_client.cc index a1dd925fcde..cbfb794817f 100644 --- a/src/client/tablet_client.cc +++ b/src/client/tablet_client.cc @@ -73,6 +73,7 @@ bool TabletClient::Query(const std::string& db, const std::string& sql, const st } bool TabletClient::Query(const std::string& db, const std::string& sql, + hybridse::vm::EngineMode default_mode, const std::vector& parameter_types, const std::string& parameter_row, brpc::Controller* cntl, ::openmldb::api::QueryResponse* response, const bool is_debug) { @@ -80,7 +81,7 @@ bool TabletClient::Query(const std::string& db, const std::string& sql, ::openmldb::api::QueryRequest request; request.set_sql(sql); request.set_db(db); - request.set_is_batch(true); + request.set_is_batch(default_mode == hybridse::vm::kBatchMode); request.set_is_debug(is_debug); request.set_parameter_row_size(parameter_row.size()); request.set_parameter_row_slices(1); diff --git a/src/client/tablet_client.h b/src/client/tablet_client.h index 177124208fc..33188adadcc 100644 --- a/src/client/tablet_client.h +++ b/src/client/tablet_client.h @@ -64,7 +64,7 @@ class TabletClient : public Client { const openmldb::common::VersionPair& pair, std::string& msg); // NOLINT - bool Query(const std::string& db, const std::string& sql, + bool Query(const std::string& db, const std::string& sql, hybridse::vm::EngineMode default_mode, const std::vector& parameter_types, const std::string& parameter_row, brpc::Controller* cntl, ::openmldb::api::QueryResponse* response, const bool is_debug = false); diff --git a/src/sdk/internal/system_variable.cc b/src/sdk/internal/system_variable.cc new file mode 100644 index 00000000000..dc818dfb412 --- /dev/null +++ b/src/sdk/internal/system_variable.cc @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2024 OpenMLDB Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "sdk/internal/system_variable.h" + +#include "absl/status/status.h" +#include "absl/strings/ascii.h" +#include "absl/strings/str_join.h" +#include "absl/strings/substitute.h" + +namespace openmldb { +namespace sdk { +namespace internal { + +static SystemVariables CreateSystemVariablePresets() { + SystemVariables map = { + {"execute_mode", {"online", "request", "offline"}}, + // TODO(someone): add all + }; + return map; +} + +const SystemVariables& GetSystemVariablePresets() { + static const SystemVariables& map = *new auto(CreateSystemVariablePresets()); + return map; +} +absl::Status CheckSystemVariableSet(absl::string_view key, absl::string_view val) { + auto& presets = GetSystemVariablePresets(); + auto it = presets.find(absl::AsciiStrToLower(key)); + if (it == presets.end()) { + return absl::InvalidArgumentError(absl::Substitute("key '$0' not found as a system variable", key)); + } + + if (it->second.find(absl::AsciiStrToLower(val)) == it->second.end()) { + return absl::InvalidArgumentError( + absl::Substitute("invalid value for system variable '$0', expect one of [$1], but got $2", key, + absl::StrJoin(it->second, ", "), val)); + } + + return absl::OkStatus(); +} +} // namespace internal +} // namespace sdk +} // namespace openmldb diff --git a/src/sdk/internal/system_variable.h b/src/sdk/internal/system_variable.h new file mode 100644 index 00000000000..75edd014ab7 --- /dev/null +++ b/src/sdk/internal/system_variable.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 OpenMLDB Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SRC_SDK_INTERNAL_SYSTEM_VARIABLE_H_ +#define SRC_SDK_INTERNAL_SYSTEM_VARIABLE_H_ + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" + +namespace openmldb { +namespace sdk { +namespace internal { + +using SystemVariables = absl::flat_hash_map>; + +const SystemVariables& GetSystemVariablePresets(); + +// check if the stmt 'set {key} = {val}' has a valid semantic +// key and value for system variable is case insensetive +absl::Status CheckSystemVariableSet(absl::string_view key, absl::string_view val); + +} // namespace internal +} // namespace sdk +} // namespace openmldb + +#endif // SRC_SDK_INTERNAL_SYSTEM_VARIABLE_H_ diff --git a/src/sdk/sql_cluster_router.cc b/src/sdk/sql_cluster_router.cc index 068538ba5e0..dbdd7dede9d 100644 --- a/src/sdk/sql_cluster_router.cc +++ b/src/sdk/sql_cluster_router.cc @@ -56,6 +56,7 @@ #include "sdk/base.h" #include "sdk/base_impl.h" #include "sdk/batch_request_result_set_sql.h" +#include "sdk/internal/system_variable.h" #include "sdk/job_table_helper.h" #include "sdk/node_adapter.h" #include "sdk/query_future_impl.h" @@ -1215,8 +1216,8 @@ std::shared_ptr<::hybridse::sdk::ResultSet> SQLClusterRouter::ExecuteSQLParamete cntl->set_timeout_ms(options_->request_timeout); DLOG(INFO) << "send query to tablet " << client->GetEndpoint(); auto response = std::make_shared<::openmldb::api::QueryResponse>(); - if (!client->Query(db, sql, parameter_types, parameter ? parameter->GetRow() : "", cntl.get(), response.get(), - options_->enable_debug)) { + if (!client->Query(db, sql, GetDefaultEngineMode(), parameter_types, parameter ? parameter->GetRow() : "", + cntl.get(), response.get(), options_->enable_debug)) { // rpc error is in cntl or response RPC_STATUS_AND_WARN(status, cntl, response, "Query rpc failed"); return {}; @@ -1995,7 +1996,7 @@ std::shared_ptr SQLClusterRouter::HandleSQLCmd(const h case hybridse::node::kCmdShowGlobalVariables: { std::string db = openmldb::nameserver::INFORMATION_SCHEMA_DB; std::string table = openmldb::nameserver::GLOBAL_VARIABLES; - std::string sql = "select * from " + table; + std::string sql = "select * from " + table + " CONFIG (execute_mode = 'online')"; ::hybridse::sdk::Status status; auto rs = ExecuteSQLParameterized(db, sql, std::shared_ptr(), &status); if (status.code != 0) { @@ -2884,9 +2885,7 @@ std::shared_ptr SQLClusterRouter::ExecuteSQL( } case hybridse::node::kPlanTypeFuncDef: case hybridse::node::kPlanTypeQuery: { - ::hybridse::vm::EngineMode default_mode = (!cluster_sdk_->IsClusterMode() || is_online_mode) - ? ::hybridse::vm::EngineMode::kBatchMode - : ::hybridse::vm::EngineMode::kOffline; + ::hybridse::vm::EngineMode default_mode = GetDefaultEngineMode(); // execute_mode in query config clause takes precedence auto mode = ::hybridse::vm::Engine::TryDetermineEngineMode(sql, default_mode); if (mode != ::hybridse::vm::EngineMode::kOffline) { @@ -3191,10 +3190,27 @@ std::shared_ptr SQLClusterRouter::ExecuteOfflineQuery( } } -bool SQLClusterRouter::IsOnlineMode() { +::hybridse::vm::EngineMode SQLClusterRouter::GetDefaultEngineMode() const { std::lock_guard<::openmldb::base::SpinMutex> lock(mu_); auto it = session_variables_.find("execute_mode"); - if (it != session_variables_.end() && it->second == "online") { + if (it != session_variables_.end()) { + // 1. infer from system variable + auto m = hybridse::vm::UnparseEngineMode(it->second).value_or(hybridse::vm::EngineMode::kBatchMode); + + // 2. standalone mode do not have offline + if (!cluster_sdk_->IsClusterMode() && m == hybridse::vm::kOffline) { + return hybridse::vm::kBatchMode; + } + return m; + } + + return hybridse::vm::EngineMode::kBatchMode; +} + +bool SQLClusterRouter::IsOnlineMode() const { + std::lock_guard<::openmldb::base::SpinMutex> lock(mu_); + auto it = session_variables_.find("execute_mode"); + if (it != session_variables_.end() && (it->second == "online" || it->second == "request")) { return true; } return false; @@ -3273,8 +3289,9 @@ ::hybridse::sdk::Status SQLClusterRouter::SetVariable(hybridse::node::SetPlanNod std::transform(value.begin(), value.end(), value.begin(), ::tolower); // TODO(hw): validation can be simpler if (key == "execute_mode") { - if (value != "online" && value != "offline") { - return {StatusCode::kCmdError, "the value of execute_mode must be online|offline"}; + auto s = sdk::internal::CheckSystemVariableSet(key, value); + if (!s.ok()) { + return {hybridse::common::kCmdError, s.ToString()}; } } else if (key == "enable_trace" || key == "sync_job") { if (value != "true" && value != "false") { diff --git a/src/sdk/sql_cluster_router.h b/src/sdk/sql_cluster_router.h index e917c170a14..e3159c0d1d7 100644 --- a/src/sdk/sql_cluster_router.h +++ b/src/sdk/sql_cluster_router.h @@ -274,7 +274,8 @@ class SQLClusterRouter : public SQLRouter { bool NotifyTableChange() override; - bool IsOnlineMode() override; + ::hybridse::vm::EngineMode GetDefaultEngineMode() const; + bool IsOnlineMode() const override; bool IsEnableTrace(); std::string GetDatabase() override; @@ -454,7 +455,7 @@ class SQLClusterRouter : public SQLRouter { DBSDK* cluster_sdk_; std::map>>> input_lru_cache_; - ::openmldb::base::SpinMutex mu_; + mutable ::openmldb::base::SpinMutex mu_; ::openmldb::base::Random rand_; std::atomic insert_memory_usage_limit_ = 0; // [0-100], the default value 0 means unlimited }; diff --git a/src/sdk/sql_router.h b/src/sdk/sql_router.h index f68d7d39a1c..55fd72b6b5e 100644 --- a/src/sdk/sql_router.h +++ b/src/sdk/sql_router.h @@ -220,7 +220,7 @@ class SQLRouter { virtual bool NotifyTableChange() = 0; - virtual bool IsOnlineMode() = 0; + virtual bool IsOnlineMode() const = 0; virtual std::string GetDatabase() = 0; diff --git a/src/sdk/sql_router_sdk.i b/src/sdk/sql_router_sdk.i index 15ea2b8e7c4..0186550e0a9 100644 --- a/src/sdk/sql_router_sdk.i +++ b/src/sdk/sql_router_sdk.i @@ -80,6 +80,7 @@ #include "sdk/sql_insert_row.h" #include "sdk/sql_delete_row.h" #include "sdk/table_reader.h" +#include "sdk/internal/system_variable.h" using hybridse::sdk::Schema; using hybridse::sdk::ColumnTypes; diff --git a/src/tablet/tablet_impl.cc b/src/tablet/tablet_impl.cc index 2f7544f2847..1545b96c9d4 100644 --- a/src/tablet/tablet_impl.cc +++ b/src/tablet/tablet_impl.cc @@ -1691,6 +1691,7 @@ void TabletImpl::ProcessQuery(bool is_sub, RpcController* ctrl, const openmldb:: auto mode = hybridse::vm::Engine::TryDetermineEngineMode(request->sql(), default_mode); ::hybridse::base::Status status; + // FIXME(someone): it does not handles batchrequest if (mode == hybridse::vm::EngineMode::kBatchMode) { // convert repeated openmldb:type::DataType into hybridse::codec::Schema hybridse::codec::Schema parameter_schema; @@ -5426,7 +5427,12 @@ void TabletImpl::RunRequestQuery(RpcController* ctrl, const openmldb::api::Query } if (ret != 0) { response.set_code(::openmldb::base::kSQLRunError); - response.set_msg("fail to run sql"); + if (ret == hybridse::common::StatusCode::kRunSessionError) { + // special handling + response.set_msg("request SQL requires a non-empty request row, but empty row received"); + } else { + response.set_msg("fail to run sql"); + } return; } else if (row.GetRowPtrCnt() != 1) { response.set_code(::openmldb::base::kSQLRunError); From cf86f04f6f64c98d6843179dcd872eeed6ddb251 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Wed, 26 Jun 2024 12:52:57 +0800 Subject: [PATCH 19/41] feat(udf): array_combine & array_join (#3945) * feat(udf): array_combine * feat(udf): new functions - array_combine - array_join * feat: casting arrays to array for array_combine WIP, string allocation need fix * fix: array_combine with non-string types * feat(array_combine): handle null inputs * fix(array_combine): behavior tweaks - use empty string if delimiter is null - restrict to array_combine(string, array ...) --- cases/query/udf_query.yaml | 92 +++++++++++++++++ hybridse/src/base/cartesian_product.cc | 62 +++++++++++ hybridse/src/base/cartesian_product.h | 32 ++++++ hybridse/src/codegen/array_ir_builder.cc | 115 +++++++++++++++++++++ hybridse/src/codegen/array_ir_builder.h | 14 ++- hybridse/src/codegen/ir_base_builder.cc | 20 +++- hybridse/src/codegen/string_ir_builder.cc | 12 +++ hybridse/src/codegen/string_ir_builder.h | 4 + hybridse/src/codegen/struct_ir_builder.cc | 96 ++++++++++++++++- hybridse/src/codegen/struct_ir_builder.h | 13 +++ hybridse/src/udf/default_defs/array_def.cc | 78 ++++++++++++++ hybridse/src/udf/udf.cc | 55 ++++++++++ hybridse/src/udf/udf.h | 3 +- hybridse/src/vm/jit_wrapper.cc | 7 ++ 14 files changed, 595 insertions(+), 8 deletions(-) create mode 100644 hybridse/src/base/cartesian_product.cc create mode 100644 hybridse/src/base/cartesian_product.h diff --git a/cases/query/udf_query.yaml b/cases/query/udf_query.yaml index ee9cad2d667..bc0cbe786fd 100644 --- a/cases/query/udf_query.yaml +++ b/cases/query/udf_query.yaml @@ -573,6 +573,98 @@ cases: - c1 bool data: | true, false + - id: array_join + mode: request-unsupport + sql: | + select + array_join(["1", "2"], ",") c1, + array_join(["1", "2"], "") c2, + array_join(["1", "2"], cast(null as string)) c3, + array_join(["1", NULL, "4", "5", NULL], "-") c4, + array_join(array[], ",") as c5 + expect: + columns: + - c1 string + - c2 string + - c3 string + - c4 string + - c5 string + rows: + - ["1,2", "12", "12", "1-4-5", ""] + - id: array_combine + mode: request-unsupport + sql: | + select + array_join(array_combine("-", ["1", "2"], ["3", "4"]), ",") c0, + expect: + columns: + - c0 string + rows: + - ["1-3,1-4,2-3,2-4"] + + - id: array_combine_2 + desc: array_combine casting array to array first + mode: request-unsupport + sql: | + select + array_join(array_combine("-", [1, 2], [3, 4]), ",") c0, + array_join(array_combine("-", [1, 2], array[3], ["5", "6"]), ",") c1, + array_join(array_combine("|", ["1"], [timestamp(1717171200000), timestamp("2024-06-02 12:00:00")]), ",") c2, + array_join(array_combine("|", ["1"]), ",") c3, + expect: + columns: + - c0 string + - c1 string + - c2 string + - c3 string + rows: + - ["1-3,1-4,2-3,2-4", "1-3-5,1-3-6,2-3-5,2-3-6", "1|2024-06-01 00:00:00,1|2024-06-02 12:00:00", "1"] + - id: array_combine_3 + desc: null values skipped + mode: request-unsupport + sql: | + select + array_join(array_combine("-", [1, NULL], [3, 4]), ",") c0, + array_join(array_combine("-", ARRAY[NULL], ["9", "8"]), ",") c1, + array_join(array_combine(string(NULL), ARRAY[1], ["9", "8"]), ",") c2, + expect: + columns: + - c0 string + - c1 string + - c2 string + rows: + - ["1-3,1-4", "", "19,18"] + - id: array_combine_4 + desc: construct array from table + mode: request-unsupport + inputs: + - name: t1 + columns: ["col1:int32", "std_ts:timestamp", "col2:string"] + indexs: ["index1:col1:std_ts"] + rows: + - [1, 1590115420001, "foo"] + - [2, 1590115420001, "bar"] + sql: | + select + col1, + array_join(array_combine("-", [col1, 10], [col2, "c2"]), ",") c0, + from t1 + expect: + columns: + - col1 int32 + - c0 string + rows: + - [1, "1-foo,1-c2,10-foo,10-c2"] + - [2, "2-bar,2-c2,10-bar,10-c2"] + - id: array_combine_err1 + mode: request-unsupport + sql: | + select + array_join(array_combine("-"), ",") c0, + expect: + success: false + msg: | + Fail to resolve expression: array_join(array_combine(-), ,) # ================================================================ # Map data type diff --git a/hybridse/src/base/cartesian_product.cc b/hybridse/src/base/cartesian_product.cc new file mode 100644 index 00000000000..f06d28edcdf --- /dev/null +++ b/hybridse/src/base/cartesian_product.cc @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2024 OpenMLDB authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "base/cartesian_product.h" + +#include + +#include "absl/types/span.h" + +namespace hybridse { +namespace base { + +static auto cartesian_product(const std::vector>& lists) { + std::vector> result; + if (std::find_if(std::begin(lists), std::end(lists), [](auto e) -> bool { return e.size() == 0; }) != + std::end(lists)) { + return result; + } + for (auto& e : lists[0]) { + result.push_back({e}); + } + for (size_t i = 1; i < lists.size(); ++i) { + std::vector> temp; + for (auto& e : result) { + for (auto f : lists[i]) { + auto e_tmp = e; + e_tmp.push_back(f); + temp.push_back(e_tmp); + } + } + result = temp; + } + return result; +} + +std::vector> cartesian_product(absl::Span vec) { + std::vector> input; + for (auto& v : vec) { + std::vector seq(v, 0); + for (int i = 0; i < v; ++i) { + seq[i] = i; + } + input.push_back(seq); + } + return cartesian_product(input); +} + +} // namespace base +} // namespace hybridse diff --git a/hybridse/src/base/cartesian_product.h b/hybridse/src/base/cartesian_product.h new file mode 100644 index 00000000000..54d4191aba1 --- /dev/null +++ b/hybridse/src/base/cartesian_product.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 OpenMLDB authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef HYBRIDSE_SRC_BASE_CARTESIAN_PRODUCT_H_ +#define HYBRIDSE_SRC_BASE_CARTESIAN_PRODUCT_H_ + +#include + +#include "absl/types/span.h" + +namespace hybridse { +namespace base { + +std::vector> cartesian_product(absl::Span vec); + +} // namespace base +} // namespace hybridse + +#endif // HYBRIDSE_SRC_BASE_CARTESIAN_PRODUCT_H_ diff --git a/hybridse/src/codegen/array_ir_builder.cc b/hybridse/src/codegen/array_ir_builder.cc index 545ccb7555b..8e4a6005800 100644 --- a/hybridse/src/codegen/array_ir_builder.cc +++ b/hybridse/src/codegen/array_ir_builder.cc @@ -18,8 +18,12 @@ #include +#include "absl/strings/substitute.h" +#include "base/fe_status.h" +#include "codegen/cast_expr_ir_builder.h" #include "codegen/context.h" #include "codegen/ir_base_builder.h" +#include "codegen/string_ir_builder.h" namespace hybridse { namespace codegen { @@ -122,5 +126,116 @@ bool ArrayIRBuilder::CreateDefault(::llvm::BasicBlock* block, ::llvm::Value** ou return true; } +absl::StatusOr ArrayIRBuilder::ExtractElement(CodeGenContextBase* ctx, const NativeValue& arr, + const NativeValue& key) const { + return absl::UnimplementedError("array extract element"); +} + +absl::StatusOr ArrayIRBuilder::NumElements(CodeGenContextBase* ctx, llvm::Value* arr) const { + llvm::Value* out = nullptr; + if (!Load(ctx->GetCurrentBlock(), arr, SZ_IDX, &out)) { + return absl::InternalError("codegen: fail to extract array size"); + } + + return out; +} + +absl::StatusOr ArrayIRBuilder::CastToArrayString(CodeGenContextBase* ctx, llvm::Value* src) { + auto sb = StructTypeIRBuilder::CreateStructTypeIRBuilder(ctx->GetModule(), src->getType()); + CHECK_ABSL_STATUSOR(sb); + + ArrayIRBuilder* src_builder = dynamic_cast(sb.value().get()); + if (!src_builder) { + return absl::InvalidArgumentError("input value not a array"); + } + + llvm::Type* src_ele_type = src_builder->element_type_; + if (IsStringPtr(src_ele_type)) { + // already array + return src; + } + + auto fields = src_builder->Load(ctx, src); + CHECK_ABSL_STATUSOR(fields); + llvm::Value* src_raws = fields.value().at(RAW_IDX); + llvm::Value* src_nulls = fields.value().at(NULL_IDX); + llvm::Value* num_elements = fields.value().at(SZ_IDX); + + llvm::Value* casted = nullptr; + if (!CreateDefault(ctx->GetCurrentBlock(), &casted)) { + return absl::InternalError("codegen error: fail to construct default array"); + } + // initialize each element + CHECK_ABSL_STATUS(Initialize(ctx, casted, {num_elements})); + + auto builder = ctx->GetBuilder(); + auto dst_fields = Load(ctx, casted); + CHECK_ABSL_STATUSOR(fields); + auto* raw_array_ptr = dst_fields.value().at(RAW_IDX); + auto* nullables_ptr = dst_fields.value().at(NULL_IDX); + + llvm::Type* idx_type = builder->getInt64Ty(); + llvm::Value* idx = builder->CreateAlloca(idx_type); + builder->CreateStore(builder->getInt64(0), idx); + CHECK_STATUS_TO_ABSL(ctx->CreateWhile( + [&](llvm::Value** cond) -> base::Status { + *cond = builder->CreateICmpSLT(builder->CreateLoad(idx_type, idx), num_elements); + return {}; + }, + [&]() -> base::Status { + llvm::Value* idx_val = builder->CreateLoad(idx_type, idx); + codegen::CastExprIRBuilder cast_builder(ctx->GetCurrentBlock()); + + llvm::Value* src_ele_value = + builder->CreateLoad(src_ele_type, builder->CreateGEP(src_ele_type, src_raws, idx_val)); + llvm::Value* dst_ele = + builder->CreateLoad(element_type_, builder->CreateGEP(element_type_, raw_array_ptr, idx_val)); + + codegen::StringIRBuilder str_builder(ctx->GetModule()); + auto s = str_builder.CastFrom(ctx->GetCurrentBlock(), src_ele_value, dst_ele); + CHECK_TRUE(s.ok(), common::kCodegenError, s.ToString()); + + builder->CreateStore( + builder->CreateLoad(builder->getInt1Ty(), builder->CreateGEP(builder->getInt1Ty(), src_nulls, idx_val)), + builder->CreateGEP(builder->getInt1Ty(), nullables_ptr, idx_val)); + + builder->CreateStore(builder->CreateAdd(idx_val, builder->getInt64(1)), idx); + return {}; + })); + + CHECK_ABSL_STATUS(Set(ctx, casted, {raw_array_ptr, nullables_ptr, num_elements})); + return casted; +} + +absl::Status ArrayIRBuilder::Initialize(CodeGenContextBase* ctx, ::llvm::Value* alloca, + absl::Span args) const { + auto* builder = ctx->GetBuilder(); + StringIRBuilder str_builder(ctx->GetModule()); + auto ele_type = str_builder.GetType(); + if (!alloca->getType()->isPointerTy() || alloca->getType()->getPointerElementType() != struct_type_ || + ele_type->getPointerTo() != element_type_) { + return absl::UnimplementedError(absl::Substitute( + "not able to Initialize array except array, got type $0", GetLlvmObjectString(alloca->getType()))); + } + if (args.size() != 1) { + // require one argument that is array size + return absl::InvalidArgumentError("initialize array requries one argument which is array size"); + } + if (!args[0]->getType()->isIntegerTy()) { + return absl::InvalidArgumentError("array size argument should be integer"); + } + auto sz = args[0]; + if (sz->getType() != builder->getInt64Ty()) { + CastExprIRBuilder cast_builder(ctx->GetCurrentBlock()); + base::Status s; + cast_builder.SafeCastNumber(sz, builder->getInt64Ty(), &sz, s); + CHECK_STATUS_TO_ABSL(s); + } + auto fn = ctx->GetModule()->getOrInsertFunction("hybridse_alloc_array_string", builder->getVoidTy(), + struct_type_->getPointerTo(), builder->getInt64Ty()); + + builder->CreateCall(fn, {alloca, sz}); + return absl::OkStatus(); +} } // namespace codegen } // namespace hybridse diff --git a/hybridse/src/codegen/array_ir_builder.h b/hybridse/src/codegen/array_ir_builder.h index a4ab9dd0a6e..9ee522b509c 100644 --- a/hybridse/src/codegen/array_ir_builder.h +++ b/hybridse/src/codegen/array_ir_builder.h @@ -42,11 +42,21 @@ class ArrayIRBuilder : public StructTypeIRBuilder { CHECK_TRUE(false, common::kCodegenError, "casting to array un-implemented"); }; - private: - void InitStructType() override; + absl::StatusOr CastToArrayString(CodeGenContextBase* ctx, llvm::Value* src); + + absl::StatusOr ExtractElement(CodeGenContextBase* ctx, const NativeValue& arr, + const NativeValue& key) const override; + + absl::StatusOr NumElements(CodeGenContextBase* ctx, llvm::Value* arr) const override; bool CreateDefault(::llvm::BasicBlock* block, ::llvm::Value** output) override; + absl::Status Initialize(CodeGenContextBase* ctx, ::llvm::Value* alloca, + absl::Span args) const override; + + private: + void InitStructType() override; + private: ::llvm::Type* element_type_ = nullptr; }; diff --git a/hybridse/src/codegen/ir_base_builder.cc b/hybridse/src/codegen/ir_base_builder.cc index 72dc7c93de7..f272cf9bda0 100644 --- a/hybridse/src/codegen/ir_base_builder.cc +++ b/hybridse/src/codegen/ir_base_builder.cc @@ -575,12 +575,12 @@ bool GetFullType(node::NodeManager* nm, ::llvm::Type* type, if (type_pointee->isStructTy()) { auto* key_type = type_pointee->getStructElementType(1); const node::TypeNode* key = nullptr; - if (key_type->isPointerTy() && !GetFullType(nm, key_type->getPointerElementType(), &key)) { + if (!key_type->isPointerTy() || !GetFullType(nm, key_type->getPointerElementType(), &key)) { return false; } const node::TypeNode* value = nullptr; auto* value_type = type_pointee->getStructElementType(2); - if (value_type->isPointerTy() && !GetFullType(nm, value_type->getPointerElementType(), &value)) { + if (!value_type->isPointerTy() || !GetFullType(nm, value_type->getPointerElementType(), &value)) { return false; } @@ -590,6 +590,22 @@ bool GetFullType(node::NodeManager* nm, ::llvm::Type* type, } return false; } + case hybridse::node::kArray: { + if (type->isPointerTy()) { + auto type_pointee = type->getPointerElementType(); + if (type_pointee->isStructTy()) { + auto* key_type = type_pointee->getStructElementType(0); + const node::TypeNode* key = nullptr; + if (!key_type->isPointerTy() || !GetFullType(nm, key_type->getPointerElementType(), &key)) { + return false; + } + + *type_node = nm->MakeNode(node::DataType::kArray, key); + return true; + } + } + return false; + } default: { *type_node = nm->MakeTypeNode(base); return true; diff --git a/hybridse/src/codegen/string_ir_builder.cc b/hybridse/src/codegen/string_ir_builder.cc index 083c907fbe4..7e677f3bd3c 100644 --- a/hybridse/src/codegen/string_ir_builder.cc +++ b/hybridse/src/codegen/string_ir_builder.cc @@ -403,5 +403,17 @@ base::Status StringIRBuilder::ConcatWS(::llvm::BasicBlock* block, *output = NativeValue::CreateWithFlag(concat_str, ret_null); return base::Status(); } +absl::Status StringIRBuilder::CastFrom(llvm::BasicBlock* block, llvm::Value* src, llvm::Value* alloca) { + if (IsStringPtr(src->getType())) { + return absl::UnimplementedError("not necessary to cast string to string"); + } + ::llvm::IRBuilder<> builder(block); + ::std::string fn_name = "string." + TypeName(src->getType()); + + auto cast_func = m_->getOrInsertFunction( + fn_name, ::llvm::FunctionType::get(builder.getVoidTy(), {src->getType(), alloca->getType()}, false)); + builder.CreateCall(cast_func, {src, alloca}); + return absl::OkStatus(); +} } // namespace codegen } // namespace hybridse diff --git a/hybridse/src/codegen/string_ir_builder.h b/hybridse/src/codegen/string_ir_builder.h index 84f73d2822d..3c6bf986a10 100644 --- a/hybridse/src/codegen/string_ir_builder.h +++ b/hybridse/src/codegen/string_ir_builder.h @@ -37,6 +37,10 @@ class StringIRBuilder : public StructTypeIRBuilder { base::Status CastFrom(::llvm::BasicBlock* block, const NativeValue& src, NativeValue* output) override; base::Status CastFrom(::llvm::BasicBlock* block, ::llvm::Value* src, ::llvm::Value** output); + // casting from {in} to string ptr alloca, alloca has allocated already. + // if {in} is string ptr already, it returns error status since generally it's not necessary to call this function + absl::Status CastFrom(llvm::BasicBlock* block, llvm::Value* in, llvm::Value* alloca); + bool NewString(::llvm::BasicBlock* block, ::llvm::Value** output); bool NewString(::llvm::BasicBlock* block, const std::string& str, ::llvm::Value** output); diff --git a/hybridse/src/codegen/struct_ir_builder.cc b/hybridse/src/codegen/struct_ir_builder.cc index 4b00b052c29..d288d05a57d 100644 --- a/hybridse/src/codegen/struct_ir_builder.cc +++ b/hybridse/src/codegen/struct_ir_builder.cc @@ -16,8 +16,11 @@ #include "codegen/struct_ir_builder.h" +#include + #include "absl/status/status.h" #include "absl/strings/substitute.h" +#include "codegen/array_ir_builder.h" #include "codegen/context.h" #include "codegen/date_ir_builder.h" #include "codegen/ir_base_builder.h" @@ -70,6 +73,16 @@ absl::StatusOr> StructTypeIRBuilder::Create } break; } + case node::DataType::kArray: { + assert(ctype->IsArray() && "logic error: not a array type"); + assert(ctype->GetGenericSize() == 1 && "logic error: not a array type"); + ::llvm::Type* ele_type = nullptr; + if (!codegen::GetLlvmType(m, ctype->GetGenericType(0), &ele_type)) { + return absl::InvalidArgumentError( + absl::Substitute("not able to casting array type: $0", GetLlvmObjectString(type))); + } + return std::make_unique(m, ele_type); + } default: { break; } @@ -181,6 +194,11 @@ absl::StatusOr StructTypeIRBuilder::ExtractElement(CodeGenContextBa absl::StrCat("extract element unimplemented for ", GetLlvmObjectString(struct_type_))); } +absl::StatusOr StructTypeIRBuilder::NumElements(CodeGenContextBase* ctx, llvm::Value* arr) const { + return absl::UnimplementedError( + absl::StrCat("element size unimplemented for ", GetLlvmObjectString(struct_type_))); +} + void StructTypeIRBuilder::EnsureOK() const { assert(struct_type_ != nullptr && "filed struct_type_ uninitialized"); // it's a identified type @@ -200,9 +218,9 @@ absl::Status StructTypeIRBuilder::Set(CodeGenContextBase* ctx, ::llvm::Value* st } if (struct_value->getType()->getPointerElementType() != struct_type_) { - return absl::InvalidArgumentError(absl::Substitute("input value has different type, expect $0 but got $1", - GetLlvmObjectString(struct_type_), - GetLlvmObjectString(struct_value->getType()))); + return absl::InvalidArgumentError( + absl::Substitute("input value has different type, expect $0 but got $1", GetLlvmObjectString(struct_type_), + GetLlvmObjectString(struct_value->getType()->getPointerElementType()))); } if (members.size() != struct_type_->getNumElements()) { @@ -229,6 +247,16 @@ absl::StatusOr> StructTypeIRBuilder::Load(CodeGenConte llvm::Value* struct_ptr) const { assert(ctx != nullptr && struct_ptr != nullptr); + if (!IsStructPtr(struct_ptr->getType())) { + return absl::InvalidArgumentError( + absl::StrCat("value not a struct pointer: ", GetLlvmObjectString(struct_ptr->getType()))); + } + if (struct_ptr->getType()->getPointerElementType() != struct_type_) { + return absl::InvalidArgumentError( + absl::Substitute("input value has different type, expect $0 but got $1", GetLlvmObjectString(struct_type_), + GetLlvmObjectString(struct_ptr->getType()->getPointerElementType()))); + } + std::vector res; res.reserve(struct_type_->getNumElements()); @@ -252,5 +280,67 @@ absl::StatusOr CreateSafeNull(::llvm::BasicBlock* block, ::llvm::Ty return NativeValue(nullptr, nullptr, type); } +absl::StatusOr Combine(CodeGenContextBase* ctx, const NativeValue delimiter, + absl::Span args) { + auto builder = ctx->GetBuilder(); + + StringIRBuilder str_builder(ctx->GetModule()); + ArrayIRBuilder arr_builder(ctx->GetModule(), str_builder.GetType()->getPointerTo()); + + llvm::Value* empty_str = nullptr; + if (!str_builder.CreateDefault(ctx->GetCurrentBlock(), &empty_str)) { + return absl::InternalError("codegen error: fail to construct empty string"); + } + llvm::Value* del = builder->CreateSelect(delimiter.GetIsNull(builder), empty_str, delimiter.GetValue(builder)); + + llvm::Type* input_arr_type = arr_builder.GetType()->getPointerTo(); + llvm::Value* empty_arr = nullptr; + if (!arr_builder.CreateDefault(ctx->GetCurrentBlock(), &empty_arr)) { + return absl::InternalError("codegen error: fail to construct empty string of array"); + } + llvm::Value* input_arrays = builder->CreateAlloca(input_arr_type, builder->getInt32(args.size()), "array_data"); + node::NodeManager nm; + std::vector casted_args(args.size()); + for (int i = 0; i < args.size(); ++i) { + const node::TypeNode* tp = nullptr; + if (!GetFullType(&nm, args.at(i).GetType(), &tp)) { + return absl::InternalError("codegen error: fail to get valid type from llvm value"); + } + if (!tp->IsArray() || tp->GetGenericSize() != 1) { + return absl::InternalError("codegen error: arguments to array_combine is not ARRAY"); + } + if (!tp->GetGenericType(0)->IsString()) { + auto s = arr_builder.CastToArrayString(ctx, args.at(i).GetRaw()); + CHECK_ABSL_STATUSOR(s); + casted_args.at(i) = NativeValue::Create(s.value()); + } else { + casted_args.at(i) = args.at(i); + } + + auto safe_str_arr = + builder->CreateSelect(casted_args.at(i).GetIsNull(builder), empty_arr, casted_args.at(i).GetRaw()); + builder->CreateStore(safe_str_arr, builder->CreateGEP(input_arr_type, input_arrays, builder->getInt32(i))); + } + + ::llvm::FunctionCallee array_combine_fn = ctx->GetModule()->getOrInsertFunction( + "hybridse_array_combine", builder->getVoidTy(), str_builder.GetType()->getPointerTo(), builder->getInt32Ty(), + input_arr_type->getPointerTo(), input_arr_type); + assert(array_combine_fn); + + llvm::Value* out = builder->CreateAlloca(arr_builder.GetType()); + builder->CreateCall(array_combine_fn, { + del, // delimiter should ensure non-null + builder->getInt32(args.size()), // num of arrays + input_arrays, // ArrayRef** + out // output string + }); + + return NativeValue::Create(out); +} + +absl::Status StructTypeIRBuilder::Initialize(CodeGenContextBase* ctx, ::llvm::Value* alloca, + absl::Span args) const { + return absl::UnimplementedError(absl::StrCat("Initialize for type ", GetLlvmObjectString(struct_type_))); +} } // namespace codegen } // namespace hybridse diff --git a/hybridse/src/codegen/struct_ir_builder.h b/hybridse/src/codegen/struct_ir_builder.h index 8529c2d7848..4c7dba732dc 100644 --- a/hybridse/src/codegen/struct_ir_builder.h +++ b/hybridse/src/codegen/struct_ir_builder.h @@ -58,6 +58,9 @@ class StructTypeIRBuilder : public TypeIRBuilder { virtual absl::StatusOr<::llvm::Value*> ConstructFromRaw(CodeGenContextBase* ctx, absl::Span<::llvm::Value* const> args) const; + virtual absl::Status Initialize(CodeGenContextBase* ctx, ::llvm::Value* alloca, + absl::Span args) const; + // Extract element value from composite data type // 1. extract from array type by index // 2. extract from struct type by field name @@ -65,6 +68,12 @@ class StructTypeIRBuilder : public TypeIRBuilder { virtual absl::StatusOr ExtractElement(CodeGenContextBase* ctx, const NativeValue& arr, const NativeValue& key) const; + // Get size of the elements inside value {arr} + // - if {arr} is array/map, return size of array/map + // - if {arr} is struct, return number of struct fields + // - otherwise report error + virtual absl::StatusOr NumElements(CodeGenContextBase* ctx, llvm::Value* arr) const; + ::llvm::Type* GetType() const; std::string GetTypeDebugString() const; @@ -100,6 +109,10 @@ class StructTypeIRBuilder : public TypeIRBuilder { // returns NativeValue{raw, is_null=true} on success, raw is ensured to be not nullptr absl::StatusOr CreateSafeNull(::llvm::BasicBlock* block, ::llvm::Type* type); +// Do the cartesian product for a list of arrry +// output a array of string, each value is a pair (A1, B2, C3...), as "A1-B2-C3-...", "-" is the delimiter +absl::StatusOr Combine(CodeGenContextBase* ctx, const NativeValue delimiter, + absl::Span args); } // namespace codegen } // namespace hybridse #endif // HYBRIDSE_SRC_CODEGEN_STRUCT_IR_BUILDER_H_ diff --git a/hybridse/src/udf/default_defs/array_def.cc b/hybridse/src/udf/default_defs/array_def.cc index b5c36bc3d7e..5f1bebfaaf6 100644 --- a/hybridse/src/udf/default_defs/array_def.cc +++ b/hybridse/src/udf/default_defs/array_def.cc @@ -15,6 +15,7 @@ */ #include "absl/strings/str_split.h" +#include "codegen/struct_ir_builder.h" #include "udf/default_udf_library.h" #include "udf/udf.h" #include "udf/udf_registry.h" @@ -101,6 +102,36 @@ void SplitString(StringRef* str, StringRef* delimeter, ArrayRef* arra } } +void array_join(ArrayRef* arr, StringRef* del, bool del_null, StringRef* out) { + int sz = 0; + for (int i = 0; i < arr->size; ++i) { + if (!arr->nullables[i]) { + if (!del_null && i > 0) { + sz += del->size_; + } + sz += arr->raw[i]->size_; + } + } + + auto buf = udf::v1::AllocManagedStringBuf(sz); + memset(buf, 0, sz); + + int32_t idx = 0; + for (int i = 0; i < arr->size; ++i) { + if (!arr->nullables[i]) { + if (!del_null && i > 0) { + memcpy(buf + idx, del->data_, del->size_); + idx += del->size_; + } + memcpy(buf + idx, arr->raw[i]->data_, arr->raw[i]->size_); + idx += arr->raw[i]->size_; + } + } + + out->data_ = buf; + out->size_ = sz; +} + // =========================================================== // // UDF Register Entry // =========================================================== // @@ -148,6 +179,53 @@ void DefaultUdfLibrary::InitArrayUdfs() { @endcode @since 0.7.0)"); + RegisterExternal("array_join") + .args, Nullable>(array_join) + .doc(R"( + @brief array_join(array, delimiter) - Concatenates the elements of the given array using the delimiter. Any null value is filtered. + + Example: + + @code{.sql} + select array_join(["1", "2"], "-"); + -- output "1-2" + @endcode + @since 0.9.2)"); + + RegisterCodeGenUdf("array_combine") + .variadic_args>( + [](UdfResolveContext* ctx, const ExprAttrNode& delimit, const std::vector& arg_attrs, + ExprAttrNode* out) -> base::Status { + CHECK_TRUE(!arg_attrs.empty(), common::kCodegenError, "at least one array required by array_combine"); + for (auto & val : arg_attrs) { + CHECK_TRUE(val.type()->IsArray(), common::kCodegenError, "argument to array_combine must be array"); + } + auto nm = ctx->node_manager(); + out->SetType(nm->MakeNode(node::kArray, nm->MakeNode(node::kVarchar))); + out->SetNullable(false); + return {}; + }, + [](codegen::CodeGenContext* ctx, codegen::NativeValue del, const std::vector& args, + const node::ExprAttrNode& return_info, codegen::NativeValue* out) -> base::Status { + auto os = codegen::Combine(ctx, del, args); + CHECK_TRUE(os.ok(), common::kCodegenError, os.status().ToString()); + *out = os.value(); + return {}; + }) + .doc(R"( + @brief array_combine(delimiter, array1, array2, ...) + + return array of strings for input array1, array2, ... doing cartesian product. Each product is joined with + {delimiter} as a string. Empty string used if {delimiter} is null. + + Example: + + @code{.sql} + select array_combine("-", ["1", "2"], ["3", "4"]); + -- output ["1-3", "1-4", "2-3", "2-4"] + @endcode + @since 0.9.2 + )"); } } // namespace udf } // namespace hybridse diff --git a/hybridse/src/udf/udf.cc b/hybridse/src/udf/udf.cc index b32d75d4ac8..e5faf25db1e 100644 --- a/hybridse/src/udf/udf.cc +++ b/hybridse/src/udf/udf.cc @@ -21,11 +21,13 @@ #include #include +#include #include "absl/strings/ascii.h" #include "absl/strings/str_replace.h" #include "absl/time/civil_time.h" #include "absl/time/time.h" +#include "base/cartesian_product.h" #include "base/iterator.h" #include "boost/date_time/gregorian/conversion.hpp" #include "boost/date_time/gregorian/parsers.hpp" @@ -1413,6 +1415,59 @@ void printLog(const char* fmt) { } } +// each variadic arg is ArrayRef* +void array_combine(codec::StringRef *del, int32_t cnt, ArrayRef **data, + ArrayRef *out) { + std::vector arr_szs(cnt, 0); + for (int32_t i = 0; i < cnt; ++i) { + auto arr = data[i]; + arr_szs.at(i) = arr->size; + } + + // cal cartesian products + auto products = hybridse::base::cartesian_product(arr_szs); + + auto real_sz = products.size(); + v1::AllocManagedArray(out, products.size()); + + for (int prod_idx = 0; prod_idx < products.size(); ++prod_idx) { + auto &prod = products.at(prod_idx); + int32_t sz = 0; + for (int i = 0; i < prod.size(); ++i) { + if (!data[i]->nullables[prod.at(i)]) { + // delimiter would be empty string if null + if (i > 0) { + sz += del->size_; + } + sz += data[i]->raw[prod.at(i)]->size_; + } else { + // null exists in current product + // the only option now is to skip + real_sz--; + continue; + } + } + auto buf = v1::AllocManagedStringBuf(sz); + int32_t idx = 0; + for (int i = 0; i < prod.size(); ++i) { + if (!data[i]->nullables[prod.at(i)]) { + if (i > 0 && del->size_ > 0) { + memcpy(buf + idx, del->data_, del->size_); + idx += del->size_; + } + memcpy(buf + idx, data[i]->raw[prod.at(i)]->data_, data[i]->raw[prod.at(i)]->size_); + idx += data[i]->raw[prod.at(i)]->size_; + } + } + + out->nullables[prod_idx] = false; + out->raw[prod_idx]->data_ = buf; + out->raw[prod_idx]->size_ = sz; + } + + out->size = real_sz; +} + } // namespace v1 bool RegisterMethod(UdfLibrary *lib, const std::string &fn_name, hybridse::node::TypeNode *ret, diff --git a/hybridse/src/udf/udf.h b/hybridse/src/udf/udf.h index b7f222433a7..480e4f89f3c 100644 --- a/hybridse/src/udf/udf.h +++ b/hybridse/src/udf/udf.h @@ -520,7 +520,8 @@ void hex(StringRef *str, StringRef *output); void unhex(StringRef *str, StringRef *output, bool* is_null); void printLog(const char* fmt); - +void array_combine(codec::StringRef *del, int32_t cnt, ArrayRef **data, + ArrayRef *out); } // namespace v1 /// \brief register native udf related methods into given UdfLibrary `lib` diff --git a/hybridse/src/vm/jit_wrapper.cc b/hybridse/src/vm/jit_wrapper.cc index d4b4df1b01d..220a7d93085 100644 --- a/hybridse/src/vm/jit_wrapper.cc +++ b/hybridse/src/vm/jit_wrapper.cc @@ -17,6 +17,8 @@ #include #include + +#include "base/cartesian_product.h" #include "glog/logging.h" #include "llvm/ExecutionEngine/JITSymbol.h" #include "llvm/ExecutionEngine/Orc/CompileUtils.h" @@ -251,6 +253,11 @@ void InitBuiltinJitSymbols(HybridSeJitWrapper* jit) { "fmod", reinterpret_cast( static_cast(&fmod))); jit->AddExternalFunction("fmodf", reinterpret_cast(&fmodf)); + + // cartesian product + jit->AddExternalFunction("hybridse_array_combine", reinterpret_cast(&hybridse::udf::v1::array_combine)); + jit->AddExternalFunction("hybridse_alloc_array_string", + reinterpret_cast(&hybridse::udf::v1::AllocManagedArray)); } } // namespace vm From e3da2a6bbfa45792ae925e27667dd38af8cd3b99 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Wed, 26 Jun 2024 16:05:58 +0800 Subject: [PATCH 20/41] feat: support batchrequest in ProcessQuery (#3938) --- src/tablet/tablet_impl.cc | 239 ++++++++++++++++++++++++-------------- 1 file changed, 151 insertions(+), 88 deletions(-) diff --git a/src/tablet/tablet_impl.cc b/src/tablet/tablet_impl.cc index 1545b96c9d4..230b5c46a09 100644 --- a/src/tablet/tablet_impl.cc +++ b/src/tablet/tablet_impl.cc @@ -20,6 +20,7 @@ #include #include #include +#include "vm/sql_compiler.h" #ifdef DISALLOW_COPY_AND_ASSIGN #undef DISALLOW_COPY_AND_ASSIGN #endif @@ -1691,98 +1692,127 @@ void TabletImpl::ProcessQuery(bool is_sub, RpcController* ctrl, const openmldb:: auto mode = hybridse::vm::Engine::TryDetermineEngineMode(request->sql(), default_mode); ::hybridse::base::Status status; - // FIXME(someone): it does not handles batchrequest - if (mode == hybridse::vm::EngineMode::kBatchMode) { - // convert repeated openmldb:type::DataType into hybridse::codec::Schema - hybridse::codec::Schema parameter_schema; - for (int i = 0; i < request->parameter_types().size(); i++) { - auto column = parameter_schema.Add(); - hybridse::type::Type hybridse_type; - - if (!openmldb::schema::SchemaAdapter::ConvertType(request->parameter_types(i), &hybridse_type)) { - response->set_msg("Invalid parameter type: " + - openmldb::type::DataType_Name(request->parameter_types(i))); - response->set_code(::openmldb::base::kSQLCompileError); - return; + switch (mode) { + case hybridse::vm::EngineMode::kBatchMode: { + // convert repeated openmldb:type::DataType into hybridse::codec::Schema + hybridse::codec::Schema parameter_schema; + for (int i = 0; i < request->parameter_types().size(); i++) { + auto column = parameter_schema.Add(); + hybridse::type::Type hybridse_type; + + if (!openmldb::schema::SchemaAdapter::ConvertType(request->parameter_types(i), &hybridse_type)) { + response->set_msg("Invalid parameter type: " + + openmldb::type::DataType_Name(request->parameter_types(i))); + response->set_code(::openmldb::base::kSQLCompileError); + return; + } + column->set_type(hybridse_type); } - column->set_type(hybridse_type); - } - ::hybridse::vm::BatchRunSession session; - if (request->is_debug()) { - session.EnableDebug(); - } - session.SetParameterSchema(parameter_schema); - { - bool ok = engine_->Get(request->sql(), request->db(), session, status); - if (!ok) { - response->set_msg(status.msg); - response->set_code(::openmldb::base::kSQLCompileError); - DLOG(WARNING) << "fail to compile sql " << request->sql() << ", message: " << status.msg; - return; + ::hybridse::vm::BatchRunSession session; + if (request->is_debug()) { + session.EnableDebug(); + } + session.SetParameterSchema(parameter_schema); + { + bool ok = engine_->Get(request->sql(), request->db(), session, status); + if (!ok) { + response->set_msg(status.msg); + response->set_code(::openmldb::base::kSQLCompileError); + DLOG(WARNING) << "fail to compile sql " << request->sql() << ", message: " << status.msg; + return; + } } - } - ::hybridse::codec::Row parameter_row; - auto& request_buf = static_cast(ctrl)->request_attachment(); - if (request->parameter_row_size() > 0 && - !codec::DecodeRpcRow(request_buf, 0, request->parameter_row_size(), request->parameter_row_slices(), - ¶meter_row)) { - response->set_code(::openmldb::base::kSQLRunError); - response->set_msg("fail to decode parameter row"); - return; - } - std::vector<::hybridse::codec::Row> output_rows; - int32_t run_ret = session.Run(parameter_row, output_rows); - if (run_ret != 0) { - response->set_msg(status.msg); - response->set_code(::openmldb::base::kSQLRunError); - DLOG(WARNING) << "fail to run sql: " << request->sql(); - return; - } - uint32_t byte_size = 0; - uint32_t count = 0; - for (auto& output_row : output_rows) { - if (FLAGS_scan_max_bytes_size > 0 && byte_size > FLAGS_scan_max_bytes_size) { - LOG(WARNING) << "reach the max byte size " << FLAGS_scan_max_bytes_size << " truncate result"; - response->set_schema(session.GetEncodedSchema()); - response->set_byte_size(byte_size); - response->set_count(count); - response->set_code(::openmldb::base::kOk); + ::hybridse::codec::Row parameter_row; + auto& request_buf = static_cast(ctrl)->request_attachment(); + if (request->parameter_row_size() > 0 && + !codec::DecodeRpcRow(request_buf, 0, request->parameter_row_size(), request->parameter_row_slices(), + ¶meter_row)) { + response->set_code(::openmldb::base::kSQLRunError); + response->set_msg("fail to decode parameter row"); + return; + } + std::vector<::hybridse::codec::Row> output_rows; + int32_t run_ret = session.Run(parameter_row, output_rows); + if (run_ret != 0) { + response->set_msg(status.msg); + response->set_code(::openmldb::base::kSQLRunError); + DLOG(WARNING) << "fail to run sql: " << request->sql(); return; } - byte_size += output_row.size(); - buf->append(reinterpret_cast(output_row.buf()), output_row.size()); - count += 1; + uint32_t byte_size = 0; + uint32_t count = 0; + for (auto& output_row : output_rows) { + if (FLAGS_scan_max_bytes_size > 0 && byte_size > FLAGS_scan_max_bytes_size) { + LOG(WARNING) << "reach the max byte size " << FLAGS_scan_max_bytes_size << " truncate result"; + response->set_schema(session.GetEncodedSchema()); + response->set_byte_size(byte_size); + response->set_count(count); + response->set_code(::openmldb::base::kOk); + return; + } + byte_size += output_row.size(); + buf->append(reinterpret_cast(output_row.buf()), output_row.size()); + count += 1; + } + response->set_schema(session.GetEncodedSchema()); + response->set_byte_size(byte_size); + response->set_count(count); + response->set_code(::openmldb::base::kOk); + DLOG(INFO) << "handle batch sql " << request->sql() << " with record cnt " << count << " byte size " + << byte_size; + break; } - response->set_schema(session.GetEncodedSchema()); - response->set_byte_size(byte_size); - response->set_count(count); - response->set_code(::openmldb::base::kOk); - DLOG(INFO) << "handle batch sql " << request->sql() << " with record cnt " << count << " byte size " - << byte_size; - } else { - ::hybridse::vm::RequestRunSession session; - if (request->is_debug()) { - session.EnableDebug(); - } - if (request->is_procedure()) { - const std::string& db_name = request->db(); - const std::string& sp_name = request->sp_name(); - std::shared_ptr request_compile_info; - { - hybridse::base::Status status; - request_compile_info = sp_cache_->GetRequestInfo(db_name, sp_name, status); - if (!status.isOK()) { - response->set_code(::openmldb::base::ReturnCode::kProcedureNotFound); + case hybridse::vm::kRequestMode: { + ::hybridse::vm::RequestRunSession session; + if (request->is_debug()) { + session.EnableDebug(); + } + if (request->is_procedure()) { + const std::string& db_name = request->db(); + const std::string& sp_name = request->sp_name(); + std::shared_ptr request_compile_info; + { + hybridse::base::Status status; + request_compile_info = sp_cache_->GetRequestInfo(db_name, sp_name, status); + if (!status.isOK()) { + response->set_code(::openmldb::base::ReturnCode::kProcedureNotFound); + response->set_msg(status.msg); + PDLOG(WARNING, status.msg.c_str()); + return; + } + } + session.SetCompileInfo(request_compile_info); + session.SetSpName(sp_name); + RunRequestQuery(ctrl, *request, session, *response, *buf); + } else { + bool ok = engine_->Get(request->sql(), request->db(), session, status); + if (!ok || session.GetCompileInfo() == nullptr) { response->set_msg(status.msg); - PDLOG(WARNING, status.msg.c_str()); + response->set_code(::openmldb::base::kSQLCompileError); + DLOG(WARNING) << "fail to compile sql in request mode:\n" << request->sql(); return; } + RunRequestQuery(ctrl, *request, session, *response, *buf); + } + const std::string& sql = session.GetCompileInfo()->GetSql(); + if (response->code() != ::openmldb::base::kOk) { + DLOG(WARNING) << "fail to run sql " << sql << " error msg: " << response->msg(); + } else { + DLOG(INFO) << "handle request sql " << sql; + } + break; + } + case hybridse::vm::kBatchRequestMode: { + // we support a simplified batch request query here + // not procedure + // no parameter input or bachrequst row + // batchrequest row must specified in CONFIG (values = ...) + ::hybridse::base::Status status; + ::hybridse::vm::BatchRequestRunSession session; + if (request->is_debug()) { + session.EnableDebug(); } - session.SetCompileInfo(request_compile_info); - session.SetSpName(sp_name); - RunRequestQuery(ctrl, *request, session, *response, *buf); - } else { bool ok = engine_->Get(request->sql(), request->db(), session, status); if (!ok || session.GetCompileInfo() == nullptr) { response->set_msg(status.msg); @@ -1790,13 +1820,46 @@ void TabletImpl::ProcessQuery(bool is_sub, RpcController* ctrl, const openmldb:: DLOG(WARNING) << "fail to compile sql in request mode:\n" << request->sql(); return; } - RunRequestQuery(ctrl, *request, session, *response, *buf); + auto info = std::dynamic_pointer_cast(session.GetCompileInfo()); + if (info && info->get_sql_context().request_rows.empty()) { + response->set_msg("batch request values must specified in SQL CONFIG (values = [...])"); + response->set_code(::openmldb::base::kSQLCompileError); + return; + } + std::vector<::hybridse::codec::Row> output_rows; + std::vector<::hybridse::codec::Row> empty_inputs; + int32_t run_ret = session.Run(empty_inputs, output_rows); + if (run_ret != 0) { + response->set_msg(status.msg); + response->set_code(::openmldb::base::kSQLRunError); + DLOG(WARNING) << "fail to run batchrequest sql: " << request->sql(); + return; + } + uint32_t byte_size = 0; + uint32_t count = 0; + for (auto& output_row : output_rows) { + if (FLAGS_scan_max_bytes_size > 0 && byte_size > FLAGS_scan_max_bytes_size) { + LOG(WARNING) << "reach the max byte size " << FLAGS_scan_max_bytes_size << " truncate result"; + response->set_schema(session.GetEncodedSchema()); + response->set_byte_size(byte_size); + response->set_count(count); + response->set_code(::openmldb::base::kOk); + return; + } + byte_size += output_row.size(); + buf->append(reinterpret_cast(output_row.buf()), output_row.size()); + count += 1; + } + response->set_schema(session.GetEncodedSchema()); + response->set_byte_size(byte_size); + response->set_count(count); + response->set_code(::openmldb::base::kOk); + break; } - const std::string& sql = session.GetCompileInfo()->GetSql(); - if (response->code() != ::openmldb::base::kOk) { - DLOG(WARNING) << "fail to run sql " << sql << " error msg: " << response->msg(); - } else { - DLOG(INFO) << "handle request sql " << sql; + default: { + response->set_msg("un-implemented execute_mode: " + hybridse::vm::EngineModeName(mode)); + response->set_code(::openmldb::base::kSQLCompileError); + break; } } } From 2a739528e2af6c2702590be219c25beefac10f9c Mon Sep 17 00:00:00 2001 From: oh2024 <162292688+oh2024@users.noreply.github.com> Date: Wed, 26 Jun 2024 17:50:58 +0800 Subject: [PATCH 21/41] feat: user authz (#3941) * feat: change user table to match mysql * feat: support user authz * fix: cean up created users --- hybridse/include/node/node_enum.h | 4 + hybridse/include/node/plan_node.h | 61 ++++++++++++ hybridse/include/node/sql_node.h | 58 +++++++++++ hybridse/src/plan/planner.cc | 16 +++ hybridse/src/planv2/ast_node_converter.cc | 90 +++++++++++++++++ hybridse/src/planv2/ast_node_converter.h | 6 ++ src/auth/user_access_manager.cc | 41 ++++++-- src/auth/user_access_manager.h | 10 +- src/base/status.h | 3 +- src/client/ns_client.cc | 29 ++++++ src/client/ns_client.h | 5 + src/cmd/sql_cmd_test.cc | 116 +++++++++++++++++++++- src/nameserver/name_server_impl.cc | 72 +++++++++++--- src/nameserver/name_server_impl.h | 5 +- src/nameserver/system_table.h | 18 ++-- src/proto/name_server.proto | 19 ++++ src/sdk/sql_cluster_router.cc | 24 +++++ 17 files changed, 543 insertions(+), 34 deletions(-) diff --git a/hybridse/include/node/node_enum.h b/hybridse/include/node/node_enum.h index 7b189aa6aac..eea2bd9a953 100644 --- a/hybridse/include/node/node_enum.h +++ b/hybridse/include/node/node_enum.h @@ -98,6 +98,8 @@ enum SqlNodeType { kColumnSchema, kCreateUserStmt, kAlterUserStmt, + kGrantStmt, + kRevokeStmt, kCallStmt, kSqlNodeTypeLast, // debug type kVariadicUdfDef, @@ -347,6 +349,8 @@ enum PlanType { kPlanTypeShow, kPlanTypeCreateUser, kPlanTypeAlterUser, + kPlanTypeGrant, + kPlanTypeRevoke, kPlanTypeCallStmt, kUnknowPlan = -1, }; diff --git a/hybridse/include/node/plan_node.h b/hybridse/include/node/plan_node.h index ec82b6a586f..0e5683c4702 100644 --- a/hybridse/include/node/plan_node.h +++ b/hybridse/include/node/plan_node.h @@ -739,6 +739,67 @@ class CreateUserPlanNode : public LeafPlanNode { const std::shared_ptr options_; }; +class GrantPlanNode : public LeafPlanNode { + public: + explicit GrantPlanNode(std::optional target_type, std::string database, std::string target, + std::vector privileges, bool is_all_privileges, + std::vector grantees, bool with_grant_option) + : LeafPlanNode(kPlanTypeGrant), + target_type_(target_type), + database_(database), + target_(target), + privileges_(privileges), + is_all_privileges_(is_all_privileges), + grantees_(grantees), + with_grant_option_(with_grant_option) {} + ~GrantPlanNode() = default; + const std::vector Privileges() const { return privileges_; } + const std::vector Grantees() const { return grantees_; } + const std::string Database() const { return database_; } + const std::string Target() const { return target_; } + const std::optional TargetType() const { return target_type_; } + const bool IsAllPrivileges() const { return is_all_privileges_; } + const bool WithGrantOption() const { return with_grant_option_; } + + private: + std::optional target_type_; + std::string database_; + std::string target_; + std::vector privileges_; + bool is_all_privileges_; + std::vector grantees_; + bool with_grant_option_; +}; + +class RevokePlanNode : public LeafPlanNode { + public: + explicit RevokePlanNode(std::optional target_type, std::string database, std::string target, + std::vector privileges, bool is_all_privileges, + std::vector grantees) + : LeafPlanNode(kPlanTypeRevoke), + target_type_(target_type), + database_(database), + target_(target), + privileges_(privileges), + is_all_privileges_(is_all_privileges), + grantees_(grantees) {} + ~RevokePlanNode() = default; + const std::vector Privileges() const { return privileges_; } + const std::vector Grantees() const { return grantees_; } + const std::string Database() const { return database_; } + const std::string Target() const { return target_; } + const std::optional TargetType() const { return target_type_; } + const bool IsAllPrivileges() const { return is_all_privileges_; } + + private: + std::optional target_type_; + std::string database_; + std::string target_; + std::vector privileges_; + bool is_all_privileges_; + std::vector grantees_; +}; + class AlterUserPlanNode : public LeafPlanNode { public: explicit AlterUserPlanNode(const std::string& name, bool if_exists, std::shared_ptr options) diff --git a/hybridse/include/node/sql_node.h b/hybridse/include/node/sql_node.h index 96ea7a94163..52542426c2a 100644 --- a/hybridse/include/node/sql_node.h +++ b/hybridse/include/node/sql_node.h @@ -2421,6 +2421,64 @@ class AlterUserNode : public SqlNode { const std::shared_ptr options_; }; +class GrantNode : public SqlNode { + public: + explicit GrantNode(std::optional target_type, std::string database, std::string target, + std::vector privileges, bool is_all_privileges, std::vector grantees, + bool with_grant_option) + : SqlNode(kGrantStmt, 0, 0), + target_type_(target_type), + database_(database), + target_(target), + privileges_(privileges), + is_all_privileges_(is_all_privileges), + grantees_(grantees), + with_grant_option_(with_grant_option) {} + const std::vector Privileges() const { return privileges_; } + const std::vector Grantees() const { return grantees_; } + const std::string Database() const { return database_; } + const std::string Target() const { return target_; } + const std::optional TargetType() const { return target_type_; } + const bool IsAllPrivileges() const { return is_all_privileges_; } + const bool WithGrantOption() const { return with_grant_option_; } + + private: + std::optional target_type_; + std::string database_; + std::string target_; + std::vector privileges_; + bool is_all_privileges_; + std::vector grantees_; + bool with_grant_option_; +}; + +class RevokeNode : public SqlNode { + public: + explicit RevokeNode(std::optional target_type, std::string database, std::string target, + std::vector privileges, bool is_all_privileges, std::vector grantees) + : SqlNode(kRevokeStmt, 0, 0), + target_type_(target_type), + database_(database), + target_(target), + privileges_(privileges), + is_all_privileges_(is_all_privileges), + grantees_(grantees) {} + const std::vector Privileges() const { return privileges_; } + const std::vector Grantees() const { return grantees_; } + const std::string Database() const { return database_; } + const std::string Target() const { return target_; } + const std::optional TargetType() const { return target_type_; } + const bool IsAllPrivileges() const { return is_all_privileges_; } + + private: + std::optional target_type_; + std::string database_; + std::string target_; + std::vector privileges_; + bool is_all_privileges_; + std::vector grantees_; +}; + class ExplainNode : public SqlNode { public: explicit ExplainNode(const QueryNode *query, node::ExplainType explain_type) diff --git a/hybridse/src/plan/planner.cc b/hybridse/src/plan/planner.cc index b2a57b4128c..3a3984c9b16 100644 --- a/hybridse/src/plan/planner.cc +++ b/hybridse/src/plan/planner.cc @@ -768,6 +768,22 @@ base::Status SimplePlanner::CreatePlanTree(const NodePointVector &parser_trees, plan_trees.push_back(create_user_plan_node); break; } + case ::hybridse::node::kGrantStmt: { + auto node = dynamic_cast(parser_tree); + auto grant_plan_node = node_manager_->MakeNode( + node->TargetType(), node->Database(), node->Target(), node->Privileges(), node->IsAllPrivileges(), + node->Grantees(), node->WithGrantOption()); + plan_trees.push_back(grant_plan_node); + break; + } + case ::hybridse::node::kRevokeStmt: { + auto node = dynamic_cast(parser_tree); + auto revoke_plan_node = node_manager_->MakeNode( + node->TargetType(), node->Database(), node->Target(), node->Privileges(), node->IsAllPrivileges(), + node->Grantees()); + plan_trees.push_back(revoke_plan_node); + break; + } case ::hybridse::node::kAlterUserStmt: { auto node = dynamic_cast(parser_tree); auto alter_user_plan_node = node_manager_->MakeNode(node->Name(), diff --git a/hybridse/src/planv2/ast_node_converter.cc b/hybridse/src/planv2/ast_node_converter.cc index a8453e1221c..23e56924ae2 100644 --- a/hybridse/src/planv2/ast_node_converter.cc +++ b/hybridse/src/planv2/ast_node_converter.cc @@ -24,6 +24,7 @@ #include "absl/strings/ascii.h" #include "absl/strings/match.h" #include "absl/types/span.h" +#include "ast_node_converter.h" #include "base/fe_status.h" #include "node/sql_node.h" #include "udf/udf.h" @@ -725,6 +726,20 @@ base::Status ConvertStatement(const zetasql::ASTStatement* statement, node::Node *output = create_user_node; break; } + case zetasql::AST_GRANT_STATEMENT: { + const zetasql::ASTGrantStatement* grant_stmt = statement->GetAsOrNull(); + node::GrantNode* grant_node = nullptr; + CHECK_STATUS(ConvertGrantStatement(grant_stmt, node_manager, &grant_node)) + *output = grant_node; + break; + } + case zetasql::AST_REVOKE_STATEMENT: { + const zetasql::ASTRevokeStatement* revoke_stmt = statement->GetAsOrNull(); + node::RevokeNode* revoke_node = nullptr; + CHECK_STATUS(ConvertRevokeStatement(revoke_stmt, node_manager, &revoke_node)) + *output = revoke_node; + break; + } case zetasql::AST_ALTER_USER_STATEMENT: { const zetasql::ASTAlterUserStatement* alter_user_stmt = statement->GetAsOrNull(); @@ -2133,6 +2148,81 @@ base::Status ConvertAlterUserStatement(const zetasql::ASTAlterUserStatement* roo return base::Status::OK(); } +base::Status ConvertGrantStatement(const zetasql::ASTGrantStatement* root, node::NodeManager* node_manager, + node::GrantNode** output) { + CHECK_TRUE(root != nullptr, common::kSqlAstError, "not an ASTGrantStatement"); + std::vector target_path; + CHECK_STATUS(AstPathExpressionToStringList(root->target_path(), target_path)); + std::optional target_type = std::nullopt; + if (root->target_type() != nullptr) { + target_type = root->target_type()->GetAsString(); + } + + std::vector privileges; + std::vector grantees; + for (auto privilege : root->privileges()->privileges()) { + if (privilege == nullptr) { + continue; + } + + auto privilege_action = privilege->privilege_action(); + if (privilege_action != nullptr) { + privileges.push_back(privilege_action->GetAsString()); + } + } + + for (auto grantee : root->grantee_list()->grantee_list()) { + if (grantee == nullptr) { + continue; + } + + std::string grantee_str; + CHECK_STATUS(AstStringLiteralToString(grantee, &grantee_str)); + grantees.push_back(grantee_str); + } + *output = node_manager->MakeNode(target_type, target_path.at(0), target_path.at(1), privileges, + root->privileges()->is_all_privileges(), grantees, + root->with_grant_option()); + return base::Status::OK(); +} + +base::Status ConvertRevokeStatement(const zetasql::ASTRevokeStatement* root, node::NodeManager* node_manager, + node::RevokeNode** output) { + CHECK_TRUE(root != nullptr, common::kSqlAstError, "not an ASTRevokeStatement"); + std::vector target_path; + CHECK_STATUS(AstPathExpressionToStringList(root->target_path(), target_path)); + std::optional target_type = std::nullopt; + if (root->target_type() != nullptr) { + target_type = root->target_type()->GetAsString(); + } + + std::vector privileges; + std::vector grantees; + for (auto privilege : root->privileges()->privileges()) { + if (privilege == nullptr) { + continue; + } + + auto privilege_action = privilege->privilege_action(); + if (privilege_action != nullptr) { + privileges.push_back(privilege_action->GetAsString()); + } + } + + for (auto grantee : root->grantee_list()->grantee_list()) { + if (grantee == nullptr) { + continue; + } + + std::string grantee_str; + CHECK_STATUS(AstStringLiteralToString(grantee, &grantee_str)); + grantees.push_back(grantee_str); + } + *output = node_manager->MakeNode(target_type, target_path.at(0), target_path.at(1), privileges, + root->privileges()->is_all_privileges(), grantees); + return base::Status::OK(); +} + base::Status ConvertCreateIndexStatement(const zetasql::ASTCreateIndexStatement* root, node::NodeManager* node_manager, node::CreateIndexNode** output) { CHECK_TRUE(nullptr != root, common::kSqlAstError, "not an ASTCreateIndexStatement") diff --git a/hybridse/src/planv2/ast_node_converter.h b/hybridse/src/planv2/ast_node_converter.h index 631569156d2..edc0fb60c50 100644 --- a/hybridse/src/planv2/ast_node_converter.h +++ b/hybridse/src/planv2/ast_node_converter.h @@ -72,6 +72,12 @@ base::Status ConvertCreateUserStatement(const zetasql::ASTCreateUserStatement* r base::Status ConvertAlterUserStatement(const zetasql::ASTAlterUserStatement* root, node::NodeManager* node_manager, node::AlterUserNode** output); +base::Status ConvertGrantStatement(const zetasql::ASTGrantStatement* root, node::NodeManager* node_manager, + node::GrantNode** output); + +base::Status ConvertRevokeStatement(const zetasql::ASTRevokeStatement* root, node::NodeManager* node_manager, + node::RevokeNode** output); + base::Status ConvertQueryNode(const zetasql::ASTQuery* root, node::NodeManager* node_manager, node::QueryNode** output); base::Status ConvertQueryExpr(const zetasql::ASTQueryExpression* query_expr, node::NodeManager* node_manager, diff --git a/src/auth/user_access_manager.cc b/src/auth/user_access_manager.cc index d668a7dc497..1f354998ef3 100644 --- a/src/auth/user_access_manager.cc +++ b/src/auth/user_access_manager.cc @@ -47,7 +47,7 @@ void UserAccessManager::StopSyncTask() { void UserAccessManager::SyncWithDB() { if (auto it_pair = user_table_iterator_factory_(::openmldb::nameserver::USER_INFO_NAME); it_pair) { - auto new_user_map = std::make_unique>(); + auto new_user_map = std::make_unique>(); auto it = it_pair->first.get(); it->SeekToFirst(); while (it->Valid()) { @@ -56,13 +56,18 @@ void UserAccessManager::SyncWithDB() { auto size = it->GetValue().size(); codec::RowView row_view(*it_pair->second.get(), buf, size); std::string host, user, password; + std::string privilege_level_str; row_view.GetStrValue(0, &host); row_view.GetStrValue(1, &user); row_view.GetStrValue(2, &password); + row_view.GetStrValue(5, &privilege_level_str); + openmldb::nameserver::PrivilegeLevel privilege_level; + ::openmldb::nameserver::PrivilegeLevel_Parse(privilege_level_str, &privilege_level); + UserRecord user_record = {password, privilege_level}; if (host == "%") { - new_user_map->emplace(user, password); + new_user_map->emplace(user, user_record); } else { - new_user_map->emplace(FormUserHost(user, host), password); + new_user_map->emplace(FormUserHost(user, host), user_record); } it->Next(); } @@ -70,12 +75,36 @@ void UserAccessManager::SyncWithDB() { } } +std::optional UserAccessManager::GetUserPassword(const std::string& host, const std::string& user) { + if (auto user_record = user_map_.Get(FormUserHost(user, host)); user_record.has_value()) { + return user_record.value().password; + } else if (auto stored_password = user_map_.Get(user); stored_password.has_value()) { + return stored_password.value().password; + } else { + return std::nullopt; + } +} + bool UserAccessManager::IsAuthenticated(const std::string& host, const std::string& user, const std::string& password) { - if (auto stored_password = user_map_.Get(FormUserHost(user, host)); stored_password.has_value()) { - return stored_password.value() == password; + if (auto user_record = user_map_.Get(FormUserHost(user, host)); user_record.has_value()) { + return user_record.value().password == password; } else if (auto stored_password = user_map_.Get(user); stored_password.has_value()) { - return stored_password.value() == password; + return stored_password.value().password == password; } return false; } + +::openmldb::nameserver::PrivilegeLevel UserAccessManager::GetPrivilegeLevel(const std::string& user_at_host) { + std::size_t at_pos = user_at_host.find('@'); + if (at_pos != std::string::npos) { + std::string user = user_at_host.substr(0, at_pos); + std::string host = user_at_host.substr(at_pos + 1); + if (auto user_record = user_map_.Get(FormUserHost(user, host)); user_record.has_value()) { + return user_record.value().privilege_level; + } else if (auto stored_password = user_map_.Get(user); stored_password.has_value()) { + return stored_password.value().privilege_level; + } + } + return ::openmldb::nameserver::PrivilegeLevel::NO_PRIVILEGE; +} } // namespace openmldb::auth diff --git a/src/auth/user_access_manager.h b/src/auth/user_access_manager.h index 996efc326c4..9de6890f93a 100644 --- a/src/auth/user_access_manager.h +++ b/src/auth/user_access_manager.h @@ -26,9 +26,15 @@ #include #include "catalog/distribute_iterator.h" +#include "proto/name_server.pb.h" #include "refreshable_map.h" namespace openmldb::auth { +struct UserRecord { + std::string password; + ::openmldb::nameserver::PrivilegeLevel privilege_level; +}; + class UserAccessManager { public: using IteratorFactory = std::function GetUserPassword(const std::string& host, const std::string& user); private: IteratorFactory user_table_iterator_factory_; - RefreshableMap user_map_; + RefreshableMap user_map_; std::thread sync_task_thread_; std::promise stop_promise_; void StartSyncTask(); diff --git a/src/base/status.h b/src/base/status.h index c7e5ec75198..a2da254e78e 100644 --- a/src/base/status.h +++ b/src/base/status.h @@ -186,7 +186,8 @@ enum ReturnCode { kRPCError = 1004, // brpc controller error // auth - kFlushPrivilegesFailed = 1100 // brpc controller error + kFlushPrivilegesFailed = 1100, // brpc controller error + kNotAuthorized = 1101 // brpc controller error }; struct Status { diff --git a/src/client/ns_client.cc b/src/client/ns_client.cc index cdeef07e521..9a4c6f4df6d 100644 --- a/src/client/ns_client.cc +++ b/src/client/ns_client.cc @@ -317,6 +317,35 @@ bool NsClient::PutUser(const std::string& host, const std::string& name, const s return false; } +bool NsClient::PutPrivilege(const std::optional target_type, const std::string database, + const std::string target, const std::vector privileges, + const bool is_all_privileges, const std::vector grantees, + const ::openmldb::nameserver::PrivilegeLevel privilege_level) { + ::openmldb::nameserver::PutPrivilegeRequest request; + if (target_type.has_value()) { + request.set_target_type(target_type.value()); + } + request.set_database(database); + request.set_target(target); + for (const auto& privilege : privileges) { + request.add_privilege(privilege); + } + request.set_is_all_privileges(is_all_privileges); + for (const auto& grantee : grantees) { + request.add_grantee(grantee); + } + + request.set_privilege_level(privilege_level); + + ::openmldb::nameserver::GeneralResponse response; + bool ok = client_.SendRequest(&::openmldb::nameserver::NameServer_Stub::PutPrivilege, &request, &response, + FLAGS_request_timeout_ms, 1); + if (ok && response.code() == 0) { + return true; + } + return false; +} + bool NsClient::DeleteUser(const std::string& host, const std::string& name) { ::openmldb::nameserver::DeleteUserRequest request; request.set_host(host); diff --git a/src/client/ns_client.h b/src/client/ns_client.h index 73a52854765..1ddd50963bf 100644 --- a/src/client/ns_client.h +++ b/src/client/ns_client.h @@ -112,6 +112,11 @@ class NsClient : public Client { bool PutUser(const std::string& host, const std::string& name, const std::string& password); // NOLINT + bool PutPrivilege(const std::optional target_type, const std::string database, + const std::string target, const std::vector privileges, const bool is_all_privileges, + const std::vector grantees, + const ::openmldb::nameserver::PrivilegeLevel privilege_level); // NOLINT + bool DeleteUser(const std::string& host, const std::string& name); // NOLINT bool DropTable(const std::string& db, const std::string& name, diff --git a/src/cmd/sql_cmd_test.cc b/src/cmd/sql_cmd_test.cc index fe8faa21504..79225eb52dd 100644 --- a/src/cmd/sql_cmd_test.cc +++ b/src/cmd/sql_cmd_test.cc @@ -243,7 +243,6 @@ TEST_P(DBSDKTest, TestUser) { ASSERT_FALSE(status.IsOK()); sr->ExecuteSQL(absl::StrCat("CREATE USER IF NOT EXISTS user1"), &status); ASSERT_TRUE(status.IsOK()); - ASSERT_TRUE(true); auto opt = sr->GetRouterOptions(); if (cs->IsClusterMode()) { auto real_opt = std::dynamic_pointer_cast(opt); @@ -280,6 +279,121 @@ TEST_P(DBSDKTest, TestUser) { ASSERT_TRUE(status.IsOK()); } +TEST_P(DBSDKTest, TestGrantCreateUser) { + auto cli = GetParam(); + cs = cli->cs; + sr = cli->sr; + hybridse::sdk::Status status; + sr->ExecuteSQL(absl::StrCat("CREATE USER user1 OPTIONS(password='123456')"), &status); + ASSERT_TRUE(status.IsOK()); + auto opt = sr->GetRouterOptions(); + if (cs->IsClusterMode()) { + auto real_opt = std::dynamic_pointer_cast(opt); + sdk::SQLRouterOptions opt1; + opt1.zk_cluster = real_opt->zk_cluster; + opt1.zk_path = real_opt->zk_path; + opt1.user = "user1"; + opt1.password = "123456"; + auto router = NewClusterSQLRouter(opt1); + ASSERT_TRUE(router != nullptr); + router->ExecuteSQL(absl::StrCat("CREATE USER user2 OPTIONS(password='123456')"), &status); + ASSERT_FALSE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("GRANT CREATE USER ON *.* TO 'user1@%'"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("CREATE USER user2 OPTIONS(password='123456')"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("DROP USER user2"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("CREATE USER user2 OPTIONS(password='123456')"), &status); + ASSERT_TRUE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("REVOKE CREATE USER ON *.* FROM 'user1@%'"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("CREATE USER user3 OPTIONS(password='123456')"), &status); + ASSERT_FALSE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("DROP USER user2"), &status); + ASSERT_FALSE(status.IsOK()); + } else { + auto real_opt = std::dynamic_pointer_cast(opt); + sdk::StandaloneOptions opt1; + opt1.host = real_opt->host; + opt1.port = real_opt->port; + opt1.user = "user1"; + opt1.password = "123456"; + auto router = NewStandaloneSQLRouter(opt1); + ASSERT_TRUE(router != nullptr); + router->ExecuteSQL(absl::StrCat("CREATE USER user2 OPTIONS(password='123456')"), &status); + ASSERT_FALSE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("GRANT CREATE USER ON *.* TO 'user1@%'"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("CREATE USER user2 OPTIONS(password='123456')"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("DROP USER user2"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("CREATE USER user2 OPTIONS(password='123456')"), &status); + ASSERT_TRUE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("REVOKE CREATE USER ON *.* FROM 'user1@%'"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("CREATE USER user3 OPTIONS(password='123456')"), &status); + ASSERT_FALSE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("DROP USER user2"), &status); + ASSERT_FALSE(status.IsOK()); + } + sr->ExecuteSQL(absl::StrCat("DROP USER IF EXISTS user1"), &status); + ASSERT_TRUE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("DROP USER IF EXISTS user2"), &status); + ASSERT_TRUE(status.IsOK()); +} + +TEST_P(DBSDKTest, TestGrantCreateUserGrantOption) { + auto cli = GetParam(); + cs = cli->cs; + sr = cli->sr; + hybridse::sdk::Status status; + sr->ExecuteSQL(absl::StrCat("CREATE USER user1 OPTIONS(password='123456')"), &status); + ASSERT_TRUE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("CREATE USER user2 OPTIONS(password='123456')"), &status); + ASSERT_TRUE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("GRANT CREATE USER ON *.* TO 'user1@%'"), &status); + ASSERT_TRUE(status.IsOK()); + + auto opt = sr->GetRouterOptions(); + if (cs->IsClusterMode()) { + auto real_opt = std::dynamic_pointer_cast(opt); + sdk::SQLRouterOptions opt1; + opt1.zk_cluster = real_opt->zk_cluster; + opt1.zk_path = real_opt->zk_path; + opt1.user = "user1"; + opt1.password = "123456"; + auto router = NewClusterSQLRouter(opt1); + ASSERT_TRUE(router != nullptr); + router->ExecuteSQL(absl::StrCat("GRANT CREATE USER ON *.* TO 'user2@%'"), &status); + ASSERT_FALSE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("GRANT CREATE USER ON *.* TO 'user1@%' WITH GRANT OPTION"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("GRANT CREATE USER ON *.* TO 'user2@%'"), &status); + ASSERT_TRUE(status.IsOK()); + } else { + auto real_opt = std::dynamic_pointer_cast(opt); + sdk::StandaloneOptions opt1; + opt1.host = real_opt->host; + opt1.port = real_opt->port; + opt1.user = "user1"; + opt1.password = "123456"; + auto router = NewStandaloneSQLRouter(opt1); + ASSERT_TRUE(router != nullptr); + router->ExecuteSQL(absl::StrCat("GRANT CREATE USER ON *.* TO 'user2@%'"), &status); + ASSERT_FALSE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("GRANT CREATE USER ON *.* TO 'user1@%' WITH GRANT OPTION"), &status); + ASSERT_TRUE(status.IsOK()); + router->ExecuteSQL(absl::StrCat("GRANT CREATE USER ON *.* TO 'user2@%'"), &status); + ASSERT_TRUE(status.IsOK()); + } + sr->ExecuteSQL(absl::StrCat("DROP USER IF EXISTS user1"), &status); + ASSERT_TRUE(status.IsOK()); + sr->ExecuteSQL(absl::StrCat("DROP USER IF EXISTS user2"), &status); + ASSERT_TRUE(status.IsOK()); +} + TEST_P(DBSDKTest, CreateDatabase) { auto cli = GetParam(); cs = cli->cs; diff --git a/src/nameserver/name_server_impl.cc b/src/nameserver/name_server_impl.cc index 9c565272fb3..5e65a7d2d94 100644 --- a/src/nameserver/name_server_impl.cc +++ b/src/nameserver/name_server_impl.cc @@ -1380,7 +1380,8 @@ void NameServerImpl::ShowTablet(RpcController* controller, const ShowTabletReque } base::Status NameServerImpl::PutUserRecord(const std::string& host, const std::string& user, - const std::string& password) { + const std::string& password, + const ::openmldb::nameserver::PrivilegeLevel privilege_level) { std::shared_ptr table_info; if (!GetTableInfo(USER_INFO_NAME, INTERNAL_DB, &table_info)) { return {ReturnCode::kTableIsNotExist, "user table does not exist"}; @@ -1391,12 +1392,8 @@ base::Status NameServerImpl::PutUserRecord(const std::string& host, const std::s row_values.push_back(user); row_values.push_back(password); row_values.push_back("0"); // password_last_changed - row_values.push_back("0"); // password_expired_time - row_values.push_back("0"); // create_time - row_values.push_back("0"); // update_time - row_values.push_back("1"); // account_type - row_values.push_back("0"); // privileges - row_values.push_back("null"); // extra_info + row_values.push_back("0"); // password_expired + row_values.push_back(PrivilegeLevel_Name(privilege_level)); // Create_user_priv std::string encoded_row; codec::RowCodec::EncodeRow(row_values, table_info->column_desc(), 1, encoded_row); @@ -1431,7 +1428,6 @@ base::Status NameServerImpl::DeleteUserRecord(const std::string& host, const std for (int meta_idx = 0; meta_idx < table_partition.partition_meta_size(); meta_idx++) { if (table_partition.partition_meta(meta_idx).is_leader() && table_partition.partition_meta(meta_idx).is_alive()) { - uint64_t cur_ts = ::baidu::common::timer::get_micros() / 1000; std::string endpoint = table_partition.partition_meta(meta_idx).endpoint(); auto table_ptr = GetTablet(endpoint); if (!table_ptr->client_->Delete(tid, 0, host + "|" + user, "index", msg)) { @@ -5640,7 +5636,8 @@ void NameServerImpl::OnLocked() { CreateDatabaseOrExit(INTERNAL_DB); if (db_table_info_[INTERNAL_DB].count(USER_INFO_NAME) == 0) { CreateSystemTableOrExit(SystemTableType::kUser); - PutUserRecord("%", "root", "1e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + PutUserRecord("%", "root", "1e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ::openmldb::nameserver::PrivilegeLevel::PRIVILEGE_WITH_GRANT_OPTION); } if (IsClusterMode()) { if (tablets_.size() < FLAGS_system_table_replica_num) { @@ -9663,15 +9660,64 @@ NameServerImpl::GetSystemTableIterator() { void NameServerImpl::PutUser(RpcController* controller, const PutUserRequest* request, GeneralResponse* response, Closure* done) { brpc::ClosureGuard done_guard(done); - auto status = PutUserRecord(request->host(), request->name(), request->password()); - base::SetResponseStatus(status, response); + brpc::Controller* brpc_controller = static_cast(controller); + + if (brpc_controller->auth_context()->is_service() || + user_access_manager_.GetPrivilegeLevel(brpc_controller->auth_context()->user()) > + ::openmldb::nameserver::PrivilegeLevel::NO_PRIVILEGE) { + auto status = PutUserRecord(request->host(), request->name(), request->password(), + ::openmldb::nameserver::PrivilegeLevel::NO_PRIVILEGE); + base::SetResponseStatus(status, response); + } else { + base::SetResponseStatus(base::ReturnCode::kNotAuthorized, "not authorized to create user", response); + } +} + +void NameServerImpl::PutPrivilege(RpcController* controller, const PutPrivilegeRequest* request, + GeneralResponse* response, Closure* done) { + brpc::ClosureGuard done_guard(done); + + for (int i = 0; i < request->privilege_size(); ++i) { + auto privilege = request->privilege(i); + if (privilege == "CREATE USER") { + brpc::Controller* brpc_controller = static_cast(controller); + if (brpc_controller->auth_context()->is_service() || + user_access_manager_.GetPrivilegeLevel(brpc_controller->auth_context()->user()) >= + ::openmldb::nameserver::PrivilegeLevel::PRIVILEGE_WITH_GRANT_OPTION) { + for (int i = 0; i < request->grantee_size(); ++i) { + auto grantee = request->grantee(i); + std::size_t at_pos = grantee.find('@'); + if (at_pos != std::string::npos) { + std::string user = grantee.substr(0, at_pos); + std::string host = grantee.substr(at_pos + 1); + auto password = user_access_manager_.GetUserPassword(host, user); + if (password.has_value()) { + auto status = PutUserRecord(host, user, password.value(), request->privilege_level()); + base::SetResponseStatus(status, response); + } + } + } + } else { + base::SetResponseStatus(base::ReturnCode::kNotAuthorized, + "not authorized to grant create user privilege", response); + } + } + } } void NameServerImpl::DeleteUser(RpcController* controller, const DeleteUserRequest* request, GeneralResponse* response, Closure* done) { brpc::ClosureGuard done_guard(done); - auto status = DeleteUserRecord(request->host(), request->name()); - base::SetResponseStatus(status, response); + brpc::Controller* brpc_controller = static_cast(controller); + + if (brpc_controller->auth_context()->is_service() || + user_access_manager_.GetPrivilegeLevel(brpc_controller->auth_context()->user()) > + ::openmldb::nameserver::PrivilegeLevel::NO_PRIVILEGE) { + auto status = DeleteUserRecord(request->host(), request->name()); + base::SetResponseStatus(status, response); + } else { + base::SetResponseStatus(base::ReturnCode::kNotAuthorized, "not authorized to create user", response); + } } bool NameServerImpl::IsAuthenticated(const std::string& host, const std::string& username, diff --git a/src/nameserver/name_server_impl.h b/src/nameserver/name_server_impl.h index dadc335c7a3..53f44cde278 100644 --- a/src/nameserver/name_server_impl.h +++ b/src/nameserver/name_server_impl.h @@ -360,6 +360,8 @@ class NameServerImpl : public NameServer { Closure* done); void PutUser(RpcController* controller, const PutUserRequest* request, GeneralResponse* response, Closure* done); + void PutPrivilege(RpcController* controller, const PutPrivilegeRequest* request, GeneralResponse* response, + Closure* done); void DeleteUser(RpcController* controller, const DeleteUserRequest* request, GeneralResponse* response, Closure* done); bool IsAuthenticated(const std::string& host, const std::string& username, const std::string& password); @@ -373,7 +375,8 @@ class NameServerImpl : public NameServer { bool GetTableInfo(const std::string& table_name, const std::string& db_name, std::shared_ptr* table_info); - base::Status PutUserRecord(const std::string& host, const std::string& user, const std::string& password); + base::Status PutUserRecord(const std::string& host, const std::string& user, const std::string& password, + const ::openmldb::nameserver::PrivilegeLevel privilege_level); base::Status DeleteUserRecord(const std::string& host, const std::string& user); base::Status FlushPrivileges(); diff --git a/src/nameserver/system_table.h b/src/nameserver/system_table.h index cda34e1798e..03c8bc2364e 100644 --- a/src/nameserver/system_table.h +++ b/src/nameserver/system_table.h @@ -163,20 +163,16 @@ class SystemTable { break; } case SystemTableType::kUser: { - SetColumnDesc("host", type::DataType::kString, table_info->add_column_desc()); - SetColumnDesc("user", type::DataType::kString, table_info->add_column_desc()); - SetColumnDesc("password", type::DataType::kString, table_info->add_column_desc()); + SetColumnDesc("Host", type::DataType::kString, table_info->add_column_desc()); + SetColumnDesc("User", type::DataType::kString, table_info->add_column_desc()); + SetColumnDesc("authentication_string", type::DataType::kString, table_info->add_column_desc()); SetColumnDesc("password_last_changed", type::DataType::kTimestamp, table_info->add_column_desc()); - SetColumnDesc("password_expired_time", type::DataType::kBigInt, table_info->add_column_desc()); - SetColumnDesc("create_time", type::DataType::kTimestamp, table_info->add_column_desc()); - SetColumnDesc("update_time", type::DataType::kTimestamp, table_info->add_column_desc()); - SetColumnDesc("account_type", type::DataType::kInt, table_info->add_column_desc()); - SetColumnDesc("privileges", type::DataType::kString, table_info->add_column_desc()); - SetColumnDesc("extra_info", type::DataType::kString, table_info->add_column_desc()); + SetColumnDesc("password_expired", type::DataType::kTimestamp, table_info->add_column_desc()); + SetColumnDesc("Create_user_priv", type::DataType::kString, table_info->add_column_desc()); auto index = table_info->add_column_key(); index->set_index_name("index"); - index->add_col_name("host"); - index->add_col_name("user"); + index->add_col_name("Host"); + index->add_col_name("User"); auto ttl = index->mutable_ttl(); ttl->set_ttl_type(::openmldb::type::kLatestTime); ttl->set_lat_ttl(1); diff --git a/src/proto/name_server.proto b/src/proto/name_server.proto index f7c8fd5c830..14cd00d6ddd 100755 --- a/src/proto/name_server.proto +++ b/src/proto/name_server.proto @@ -544,6 +544,22 @@ message DeleteUserRequest { required string name = 2; } +enum PrivilegeLevel { + NO_PRIVILEGE = 0; + PRIVILEGE = 1; + PRIVILEGE_WITH_GRANT_OPTION = 2; +} + +message PutPrivilegeRequest { + repeated string grantee = 1; + repeated string privilege = 2; + optional string target_type = 3; + required string database = 4; + required string target = 5; + required bool is_all_privileges = 6; + required PrivilegeLevel privilege_level = 7; +} + message DeploySQLRequest { optional openmldb.api.ProcedureInfo sp_info = 3; repeated TableIndex index = 4; @@ -617,4 +633,7 @@ service NameServer { // user related interfaces rpc PutUser(PutUserRequest) returns (GeneralResponse); rpc DeleteUser(DeleteUserRequest) returns (GeneralResponse); + + // authz related interfaces + rpc PutPrivilege(PutPrivilegeRequest) returns (GeneralResponse); } diff --git a/src/sdk/sql_cluster_router.cc b/src/sdk/sql_cluster_router.cc index dbdd7dede9d..3d09156fdcc 100644 --- a/src/sdk/sql_cluster_router.cc +++ b/src/sdk/sql_cluster_router.cc @@ -2786,6 +2786,30 @@ std::shared_ptr SQLClusterRouter::ExecuteSQL( } return {}; } + case hybridse::node::kPlanTypeGrant: { + auto grant_node = dynamic_cast(node); + auto ns = cluster_sdk_->GetNsClient(); + auto ok = ns->PutPrivilege(grant_node->TargetType(), grant_node->Database(), grant_node->Target(), + grant_node->Privileges(), grant_node->IsAllPrivileges(), grant_node->Grantees(), + grant_node->WithGrantOption() + ? ::openmldb::nameserver::PrivilegeLevel::PRIVILEGE_WITH_GRANT_OPTION + : ::openmldb::nameserver::PrivilegeLevel::PRIVILEGE); + if (!ok) { + *status = {StatusCode::kCmdError, "Grant API call failed"}; + } + return {}; + } + case hybridse::node::kPlanTypeRevoke: { + auto revoke_node = dynamic_cast(node); + auto ns = cluster_sdk_->GetNsClient(); + auto ok = ns->PutPrivilege(revoke_node->TargetType(), revoke_node->Database(), revoke_node->Target(), + revoke_node->Privileges(), revoke_node->IsAllPrivileges(), + revoke_node->Grantees(), ::openmldb::nameserver::PrivilegeLevel::NO_PRIVILEGE); + if (!ok) { + *status = {StatusCode::kCmdError, "Revoke API call failed"}; + } + return {}; + } case hybridse::node::kPlanTypeAlterUser: { auto alter_node = dynamic_cast(node); UserInfo user_info; From 289b746bc302388605d31c2c8d4e256a54f0ca46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:26:01 +0800 Subject: [PATCH 22/41] build(deps-dev): bump requests from 2.31.0 to 2.32.2 in /docs (#3951) Bumps [requests](https://github.com/psf/requests) from 2.31.0 to 2.32.2. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.31.0...v2.32.2) --- updated-dependencies: - dependency-name: requests dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/poetry.lock b/docs/poetry.lock index 39577275304..2e0e52f5a21 100644 --- a/docs/poetry.lock +++ b/docs/poetry.lock @@ -425,13 +425,13 @@ files = [ [[package]] name = "requests" -version = "2.31.0" +version = "2.32.2" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, + {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, ] [package.dependencies] From ca7f7e2085c3634e2087668d88b6fa320a75190a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:26:12 +0800 Subject: [PATCH 23/41] build(deps-dev): bump org.apache.derby:derby (#3949) Bumps org.apache.derby:derby from 10.14.2.0 to 10.17.1.0. --- updated-dependencies: - dependency-name: org.apache.derby:derby dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/kafka-connect-jdbc/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/kafka-connect-jdbc/pom.xml b/extensions/kafka-connect-jdbc/pom.xml index a0c3cf0512d..2f35d1bd1e1 100644 --- a/extensions/kafka-connect-jdbc/pom.xml +++ b/extensions/kafka-connect-jdbc/pom.xml @@ -53,7 +53,7 @@ - 10.14.2.0 + 10.17.1.0 2.7 0.11.1 3.41.2.2 From 25bd745badeefd54d3f8b0c75c8c5d69ceb58a42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:26:18 +0800 Subject: [PATCH 24/41] build(deps): bump org.postgresql:postgresql (#3950) Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.3.3 to 42.3.9. - [Release notes](https://github.com/pgjdbc/pgjdbc/releases) - [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md) - [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.3.3...REL42.3.9) --- updated-dependencies: - dependency-name: org.postgresql:postgresql dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- extensions/kafka-connect-jdbc/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/kafka-connect-jdbc/pom.xml b/extensions/kafka-connect-jdbc/pom.xml index 2f35d1bd1e1..5d911a342c1 100644 --- a/extensions/kafka-connect-jdbc/pom.xml +++ b/extensions/kafka-connect-jdbc/pom.xml @@ -59,7 +59,7 @@ 3.41.2.2 19.7.0.0 8.4.1.jre8 - 42.3.3 + 42.3.9 1.3.1 0.8.5 Confluent Community License From 1c1e2134316c2cdf51be1d55beb9c41851cc8175 Mon Sep 17 00:00:00 2001 From: HuangWei Date: Mon, 1 Jul 2024 11:45:43 +0800 Subject: [PATCH 25/41] feat: iot table (#3944) * feat: iot table * fix * fix * fix delete key entry * fix comment * ut * ut test * fix ut * sleep more for truncate * sleep 16 * tool pytest fix and swig fix * fix * clean * move to base * fix * fix coverage ut * fix --------- Co-authored-by: Huang Wei --- .../ddl/CREATE_INDEX_STATEMENT.md | 6 + .../ddl/CREATE_TABLE_STATEMENT.md | 23 +- hybridse/include/node/node_manager.h | 4 +- hybridse/include/node/sql_node.h | 13 +- hybridse/src/node/node_manager.cc | 9 +- hybridse/src/node/plan_node_test.cc | 2 +- hybridse/src/node/sql_node.cc | 2 + hybridse/src/node/sql_node_test.cc | 2 +- hybridse/src/plan/planner.cc | 3 +- hybridse/src/planv2/ast_node_converter.cc | 37 +- hybridse/src/sdk/codec_sdk.cc | 2 +- src/base/index_util.cc | 121 ++++ src/base/index_util.h | 45 ++ src/base/status.h | 2 +- src/base/status_util.h | 7 + src/catalog/distribute_iterator.cc | 2 +- src/client/tablet_client.cc | 147 ++-- src/client/tablet_client.h | 16 +- src/cmd/display.h | 3 +- src/cmd/sql_cmd_test.cc | 18 +- src/codec/field_codec.h | 4 +- src/flags.cc | 4 + src/nameserver/name_server_impl.cc | 20 +- src/proto/common.proto | 7 + src/proto/tablet.proto | 1 + src/sdk/node_adapter.cc | 7 + src/sdk/option.h | 14 +- src/sdk/sql_cluster_router.cc | 364 ++++++++-- src/storage/index_organized_table.cc | 634 ++++++++++++++++++ src/storage/index_organized_table.h | 68 ++ src/storage/iot_segment.cc | 412 ++++++++++++ src/storage/iot_segment.h | 298 ++++++++ src/storage/iot_segment_test.cc | 517 ++++++++++++++ src/storage/key_entry.cc | 2 +- src/storage/mem_table.cc | 72 +- src/storage/mem_table.h | 25 +- src/storage/mem_table_iterator.cc | 7 +- src/storage/mem_table_iterator.h | 24 +- src/storage/node_cache.cc | 5 +- src/storage/record.h | 17 +- src/storage/schema.cc | 25 +- src/storage/schema.h | 35 +- src/storage/segment.cc | 81 ++- src/storage/segment.h | 25 +- src/storage/table.cc | 4 +- src/storage/table_iterator_test.cc | 12 +- src/tablet/tablet_impl.cc | 214 ++++-- src/tablet/tablet_impl_test.cc | 14 +- steps/test_python.sh | 4 +- tools/tool.py | 11 +- 50 files changed, 3082 insertions(+), 309 deletions(-) create mode 100644 src/base/index_util.cc create mode 100644 src/base/index_util.h create mode 100644 src/storage/index_organized_table.cc create mode 100644 src/storage/index_organized_table.h create mode 100644 src/storage/iot_segment.cc create mode 100644 src/storage/iot_segment.h create mode 100644 src/storage/iot_segment_test.cc diff --git a/docs/zh/openmldb_sql/ddl/CREATE_INDEX_STATEMENT.md b/docs/zh/openmldb_sql/ddl/CREATE_INDEX_STATEMENT.md index abfa201ab29..0ed5661e993 100644 --- a/docs/zh/openmldb_sql/ddl/CREATE_INDEX_STATEMENT.md +++ b/docs/zh/openmldb_sql/ddl/CREATE_INDEX_STATEMENT.md @@ -55,6 +55,12 @@ CREATE INDEX index3 ON t5 (col3) OPTIONS (ts=ts1, ttl_type=absolute, ttl=30d); ``` 关于`TTL`和`TTL_TYPE`的更多信息参考[这里](./CREATE_TABLE_STATEMENT.md) +IOT表创建不同类型的索引,不指定type创建Covering索引,指定type为secondary,创建Secondary索引: +```SQL +CREATE INDEX index_s ON t5 (col3) OPTIONS (ts=ts1, ttl_type=absolute, ttl=30d, type=secondary); +``` +同keys和ts列的索引被视为同一个索引,不要尝试建立不同type的同一索引。 + ## 相关SQL [DROP INDEX](./DROP_INDEX_STATEMENT.md) diff --git a/docs/zh/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md b/docs/zh/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md index 750b198d897..895cd5f43f6 100644 --- a/docs/zh/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md +++ b/docs/zh/openmldb_sql/ddl/CREATE_TABLE_STATEMENT.md @@ -223,7 +223,7 @@ IndexOption ::= | 配置项 | 描述 | expr | 用法示例 | |------------|---------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------| -| `KEY` | 索引列(必选)。OpenMLDB支持单列索引,也支持联合索引。当`KEY`后只有一列时,仅在该列上建立索引。当`KEY`后有多列时,建立这几列的联合索引:将多列按顺序拼接成一个字符串作为索引。 | 支持单列索引:`ColumnName`
或联合索引:
`(ColumnName (, ColumnName)* ) ` | 单列索引:`INDEX(KEY=col1)`
联合索引:`INDEX(KEY=(col1, col2))` | +| `KEY/CKEY/SKEY` | 索引列(必选)。OpenMLDB支持单列索引,也支持联合索引。当`KEY`后只有一列时,仅在该列上建立索引。当`KEY`后有多列时,建立这几列的联合索引:将多列按顺序拼接成一个字符串作为索引。多KEY使用见[Index-Orgnized Table(IOT)](#index-orgnized-tableiot)。 | 支持单列索引:`ColumnName`
或联合索引:
`(ColumnName (, ColumnName)* ) ` | 单列索引:`INDEX(KEY=col1)`
联合索引:`INDEX(KEY=(col1, col2))` | | `TS` | 索引时间列(可选)。同一个索引上的数据将按照时间索引列排序。当不显式配置`TS`时,使用数据插入的时间戳作为索引时间。时间列的类型只能为BigInt或者Timestamp | `ColumnName` | `INDEX(KEY=col1, TS=std_time)`。索引列为col1,col1相同的数据行按std_time排序。 | | `TTL_TYPE` | 淘汰规则(可选)。包括四种类型,当不显式配置`TTL_TYPE`时,默认使用`ABSOLUTE`过期配置。 | 支持的expr如下:`ABSOLUTE`
`LATEST`
`ABSORLAT`
`ABSANDLAT`。 | 具体用法可以参考下文“TTL和TTL_TYPE的配置细则” | | `TTL` | 最大存活时间/条数(可选)。依赖于`TTL_TYPE`,不同的`TTL_TYPE`有不同的`TTL` 配置方式。当不显式配置`TTL`时,`TTL=0`,表示不设置淘汰规则,OpenMLDB将不会淘汰记录。 | 支持数值:`int_literal`
或数值带时间单位(`S,M,H,D`):`interval_literal`
或元组形式:`( interval_literal , int_literal )` |具体用法可以参考下文“TTL和TTL_TYPE的配置细则” | @@ -240,6 +240,27 @@ IndexOption ::= ```{note} 最大过期时间和最大存活条数的限制,是出于性能考虑。如果你一定要配置更大的TTL值,可先创建表时临时使用合规的TTL值,然后使用nameserver的UpdateTTL接口来调整到所需的值(可无视max限制),生效需要经过一个gc时间;或者,调整nameserver配置`absolute_ttl_max`和`latest_ttl_max`,重启生效后再创建表。 ``` +#### Index-Orgnized Table(IOT) + +索引使用KEY设置时创建Covering索引,在OpenMLDB中Covering索引存储完整的数据行,也因此占用内存较多。如果希望内存占用更低,同时允许性能损失,可以使用IOT表。IOT表中可以建三种类型的索引: +- `CKEY`:Clustered索引,存完整数据行。配置的CKEY+TS用于唯一标识一行数据,INSERT重复主键时将更新数据(会触发所有索引上的删除旧数据,再INSERT新数据,性能会有损失)。也可只使用CKEY,不配置TS,CKEY唯一标识一行数据。查询到此索引的性能无损失。 +- `SKEY`:Secondary索引,存主键。不配置TS时,同SKEY下按插入时间排序。查询时先在Secondary索引中找到对应主键值,再根据主键查数据,查询性能有损失。 +- `KEY`:Covering索引,存完整数据行。不配置TS时,同KEY下按插入时间排序。查询到此索引的性能无损失。 + +创建IOT表,第一个索引必须是唯一一个Clustered索引,其他索引可选。暂不支持调整Clustered索引的顺序。 + +```sql +CREATE TABLE iot (c1 int64, c2 int64, c3 int64, INDEX(ckey=c1, ts=c2)); -- 一个Clustered索引 +CREATE TABLE iot (c1 int64, c2 int64, c3 int64, INDEX(ckey=c1), INDEX(skey=c2)); -- 一个Clustered索引和一个Secondary索引 +CREATE TABLE iot (c1 int64, c2 int64, c3 int64, INDEX(ckey=c1), INDEX(skey=c2), INDEX(key=c3)); -- 一个Clustered索引、一个Secondary索引和一个Covering索引 +``` + +IOT各个索引的TTL与普通表的不同点是,IOT Clustered索引的ttl淘汰,将触发其他索引的删除操作,而Secondary索引和Covering索引的ttl淘汰,只会删除自身索引中的数据,不会触发其他索引的删除操作。通常来讲,除非有必要让Secondary和Covering索引更加节约内存,可以只设置Clustered索引的ttl,不设置Secondary和Covering索引的ttl。 + +##### 注意事项 + +- IOT表不可以并发写入相同主键的多条数据,可能出现冲突,至少一条数据会写入失败。IOT表中已存在的相同主键的数据不需要额外处理,将会被覆盖。为了不用修复导入,请在导入前做好数据清洗,对导入数据中相同主键的数据进行去重。(覆盖会出触发所有索引中的删除,单线程写入效率也非常低,所以并不推荐单线程导入。) +- #### Example **示例1:创建一张带单列索引的表** diff --git a/hybridse/include/node/node_manager.h b/hybridse/include/node/node_manager.h index 9fc217d6f82..bc29b484f16 100644 --- a/hybridse/include/node/node_manager.h +++ b/hybridse/include/node/node_manager.h @@ -173,8 +173,8 @@ class NodeManager { SqlNode *MakeColumnIndexNode(SqlNodeList *keys, SqlNode *ts, SqlNode *ttl, SqlNode *version); SqlNode *MakeColumnIndexNode(SqlNodeList *index_item_list); - SqlNode *MakeIndexKeyNode(const std::string &key); - SqlNode *MakeIndexKeyNode(const std::vector &keys); + SqlNode *MakeIndexKeyNode(const std::string &key, const std::string &type); + SqlNode *MakeIndexKeyNode(const std::vector &keys, const std::string &type); SqlNode *MakeIndexTsNode(const std::string &ts); SqlNode *MakeIndexTTLNode(ExprListNode *ttl_expr); SqlNode *MakeIndexTTLTypeNode(const std::string &ttl_type); diff --git a/hybridse/include/node/sql_node.h b/hybridse/include/node/sql_node.h index 52542426c2a..eec63833617 100644 --- a/hybridse/include/node/sql_node.h +++ b/hybridse/include/node/sql_node.h @@ -2084,14 +2084,19 @@ class CreateStmt : public SqlNode { class IndexKeyNode : public SqlNode { public: IndexKeyNode() : SqlNode(kIndexKey, 0, 0) {} - explicit IndexKeyNode(const std::string &key) : SqlNode(kIndexKey, 0, 0), key_({key}) {} - explicit IndexKeyNode(const std::vector &keys) : SqlNode(kIndexKey, 0, 0), key_(keys) {} + explicit IndexKeyNode(const std::string &key, const std::string &type) + : SqlNode(kIndexKey, 0, 0), key_({key}), index_type_(type) {} + explicit IndexKeyNode(const std::vector &keys, const std::string &type) + : SqlNode(kIndexKey, 0, 0), key_(keys), index_type_(type) {} ~IndexKeyNode() {} void AddKey(const std::string &key) { key_.push_back(key); } + void SetIndexType(const std::string &type) { index_type_ = type; } std::vector &GetKey() { return key_; } + std::string &GetIndexType() { return index_type_; } private: std::vector key_; + std::string index_type_ = "key"; }; class IndexVersionNode : public SqlNode { public: @@ -2145,6 +2150,7 @@ class ColumnIndexNode : public SqlNode { public: ColumnIndexNode() : SqlNode(kColumnIndex, 0, 0), + index_type_("key"), ts_(""), version_(""), version_count_(0), @@ -2155,6 +2161,8 @@ class ColumnIndexNode : public SqlNode { std::vector &GetKey() { return key_; } void SetKey(const std::vector &key) { key_ = key; } + void SetIndexType(const std::string &type) { index_type_ = type; } + std::string &GetIndexType() { return index_type_; } std::string GetTs() const { return ts_; } @@ -2183,6 +2191,7 @@ class ColumnIndexNode : public SqlNode { private: std::vector key_; + std::string index_type_; std::string ts_; std::string version_; int version_count_; diff --git a/hybridse/src/node/node_manager.cc b/hybridse/src/node/node_manager.cc index ffa1fe2092f..91936235000 100644 --- a/hybridse/src/node/node_manager.cc +++ b/hybridse/src/node/node_manager.cc @@ -451,6 +451,7 @@ SqlNode *NodeManager::MakeColumnIndexNode(SqlNodeList *index_item_list) { switch (node_ptr->GetType()) { case kIndexKey: index_ptr->SetKey(dynamic_cast(node_ptr)->GetKey()); + index_ptr->SetIndexType(dynamic_cast(node_ptr)->GetIndexType()); break; case kIndexTs: index_ptr->SetTs(dynamic_cast(node_ptr)->GetColumnName()); @@ -649,12 +650,12 @@ FnParaNode *NodeManager::MakeFnParaNode(const std::string &name, const TypeNode ::hybridse::node::FnParaNode *para_node = new ::hybridse::node::FnParaNode(expr_id); return RegisterNode(para_node); } -SqlNode *NodeManager::MakeIndexKeyNode(const std::string &key) { - SqlNode *node_ptr = new IndexKeyNode(key); +SqlNode *NodeManager::MakeIndexKeyNode(const std::string &key, const std::string &type) { + SqlNode *node_ptr = new IndexKeyNode(key, type); return RegisterNode(node_ptr); } -SqlNode *NodeManager::MakeIndexKeyNode(const std::vector &keys) { - SqlNode *node_ptr = new IndexKeyNode(keys); +SqlNode *NodeManager::MakeIndexKeyNode(const std::vector &keys, const std::string &type) { + SqlNode *node_ptr = new IndexKeyNode(keys, type); return RegisterNode(node_ptr); } SqlNode *NodeManager::MakeIndexTsNode(const std::string &ts) { diff --git a/hybridse/src/node/plan_node_test.cc b/hybridse/src/node/plan_node_test.cc index aac111f8bf3..68eb0349a71 100644 --- a/hybridse/src/node/plan_node_test.cc +++ b/hybridse/src/node/plan_node_test.cc @@ -228,7 +228,7 @@ TEST_F(PlanNodeTest, MultiPlanNodeTest) { TEST_F(PlanNodeTest, ExtractColumnsAndIndexsTest) { SqlNodeList *index_items = manager_->MakeNodeList(); - index_items->PushBack(manager_->MakeIndexKeyNode("col4")); + index_items->PushBack(manager_->MakeIndexKeyNode("col4", "key")); index_items->PushBack(manager_->MakeIndexTsNode("col5")); ColumnIndexNode *index_node = dynamic_cast(manager_->MakeColumnIndexNode(index_items)); index_node->SetName("index1"); diff --git a/hybridse/src/node/sql_node.cc b/hybridse/src/node/sql_node.cc index 5055b7dabb2..05dc87e34d6 100644 --- a/hybridse/src/node/sql_node.cc +++ b/hybridse/src/node/sql_node.cc @@ -1188,6 +1188,8 @@ static absl::flat_hash_map CreateSqlNodeTypeToNa {kCreateFunctionStmt, "kCreateFunctionStmt"}, {kCreateUserStmt, "kCreateUserStmt"}, {kAlterUserStmt, "kAlterUserStmt"}, + {kRevokeStmt, "kRevokeStmt"}, + {kGrantStmt, "kGrantStmt"}, {kDynamicUdfFnDef, "kDynamicUdfFnDef"}, {kDynamicUdafFnDef, "kDynamicUdafFnDef"}, {kWithClauseEntry, "kWithClauseEntry"}, diff --git a/hybridse/src/node/sql_node_test.cc b/hybridse/src/node/sql_node_test.cc index 67bb861a812..c67a21b31d7 100644 --- a/hybridse/src/node/sql_node_test.cc +++ b/hybridse/src/node/sql_node_test.cc @@ -666,7 +666,7 @@ TEST_F(SqlNodeTest, IndexVersionNodeTest) { TEST_F(SqlNodeTest, CreateIndexNodeTest) { SqlNodeList *index_items = node_manager_->MakeNodeList(); - index_items->PushBack(node_manager_->MakeIndexKeyNode("col4")); + index_items->PushBack(node_manager_->MakeIndexKeyNode("col4", "key")); index_items->PushBack(node_manager_->MakeIndexTsNode("col5")); ColumnIndexNode *index_node = dynamic_cast(node_manager_->MakeColumnIndexNode(index_items)); CreatePlanNode *node = node_manager_->MakeCreateTablePlanNode( diff --git a/hybridse/src/plan/planner.cc b/hybridse/src/plan/planner.cc index 3a3984c9b16..1dfed24f39a 100644 --- a/hybridse/src/plan/planner.cc +++ b/hybridse/src/plan/planner.cc @@ -1139,7 +1139,7 @@ bool Planner::ExpandCurrentHistoryWindow(std::vector index_names; @@ -1199,7 +1199,6 @@ base::Status Planner::TransformTableDef(const std::string &table_name, const Nod case node::kColumnIndex: { node::ColumnIndexNode *column_index = static_cast(column_desc); - if (column_index->GetName().empty()) { column_index->SetName(PlanAPI::GenerateName("INDEX", table->indexes_size())); } diff --git a/hybridse/src/planv2/ast_node_converter.cc b/hybridse/src/planv2/ast_node_converter.cc index 23e56924ae2..ae6f7815ff2 100644 --- a/hybridse/src/planv2/ast_node_converter.cc +++ b/hybridse/src/planv2/ast_node_converter.cc @@ -1598,7 +1598,7 @@ base::Status ConvertColumnIndexNode(const zetasql::ASTIndexDefinition* ast_def_n } // case entry->name() -// "key" -> IndexKeyNode +// "key"/"ckey"/"skey" -> IndexKeyNode // "ts" -> IndexTsNode // "ttl" -> IndexTTLNode // "ttl_type" -> IndexTTLTypeNode @@ -1607,14 +1607,13 @@ base::Status ConvertIndexOption(const zetasql::ASTOptionsEntry* entry, node::Nod node::SqlNode** output) { auto name = entry->name()->GetAsString(); absl::string_view name_v(name); - if (absl::EqualsIgnoreCase("key", name_v)) { + if (absl::EqualsIgnoreCase("key", name_v) || absl::EqualsIgnoreCase("ckey", name_v) || absl::EqualsIgnoreCase("skey", name_v)) { switch (entry->value()->node_kind()) { case zetasql::AST_PATH_EXPRESSION: { std::string column_name; CHECK_STATUS( AstPathExpressionToString(entry->value()->GetAsOrNull(), &column_name)); - *output = node_manager->MakeIndexKeyNode(column_name); - + *output = node_manager->MakeIndexKeyNode(column_name, absl::AsciiStrToLower(name_v)); return base::Status::OK(); } case zetasql::AST_STRUCT_CONSTRUCTOR_WITH_PARENS: { @@ -1632,7 +1631,7 @@ base::Status ConvertIndexOption(const zetasql::ASTOptionsEntry* entry, node::Nod ast_struct_expr->field_expression(0)->GetAsOrNull(), &key_str)); node::IndexKeyNode* index_keys = - dynamic_cast(node_manager->MakeIndexKeyNode(key_str)); + dynamic_cast(node_manager->MakeIndexKeyNode(key_str, absl::AsciiStrToLower(name_v))); for (int i = 1; i < field_expr_len; ++i) { std::string key; @@ -1643,7 +1642,6 @@ base::Status ConvertIndexOption(const zetasql::ASTOptionsEntry* entry, node::Nod index_keys->AddKey(key); } *output = index_keys; - return base::Status::OK(); } default: { @@ -2256,13 +2254,34 @@ base::Status ConvertCreateIndexStatement(const zetasql::ASTCreateIndexStatement* keys.push_back(path.back()); } node::SqlNodeList* index_node_list = node_manager->MakeNodeList(); - - node::SqlNode* index_key_node = node_manager->MakeIndexKeyNode(keys); + // extract index type from options + std::string index_type{"key"}; + if (root->options_list() != nullptr) { + for (const auto option : root->options_list()->options_entries()) { + if (auto name = option->name()->GetAsString(); absl::EqualsIgnoreCase(name, "type")) { + CHECK_TRUE(option->value()->node_kind() == zetasql::AST_PATH_EXPRESSION, common::kSqlAstError, + "Invalid index type, should be path expression"); + std::string type_name; + CHECK_STATUS( + AstPathExpressionToString(option->value()->GetAsOrNull(), &type_name)); + if (absl::EqualsIgnoreCase(type_name, "secondary")) { + index_type = "skey"; + } else if (!absl::EqualsIgnoreCase(type_name, "covering")) { + FAIL_STATUS(common::kSqlAstError, "Invalid index type: ", type_name); + } + } + } + } + node::SqlNode* index_key_node = node_manager->MakeIndexKeyNode(keys, index_type); index_node_list->PushBack(index_key_node); if (root->options_list() != nullptr) { for (const auto option : root->options_list()->options_entries()) { + // ignore type + if (auto name = option->name()->GetAsString(); absl::EqualsIgnoreCase(name, "type")) { + continue; + } node::SqlNode* node = nullptr; - CHECK_STATUS(ConvertIndexOption(option, node_manager, &node)); + CHECK_STATUS(ConvertIndexOption(option, node_manager, &node)); // option set secondary index type if (node != nullptr) { // NOTE: unhandled option will return OK, but node is not set index_node_list->PushBack(node); diff --git a/hybridse/src/sdk/codec_sdk.cc b/hybridse/src/sdk/codec_sdk.cc index 9b910dd28cd..c09216b2600 100644 --- a/hybridse/src/sdk/codec_sdk.cc +++ b/hybridse/src/sdk/codec_sdk.cc @@ -73,7 +73,7 @@ bool RowIOBufView::Reset(const butil::IOBuf& buf) { return false; } str_addr_length_ = codec::GetAddrLength(size_); - DLOG(INFO) << "size " << size_ << " addr length " << str_addr_length_; + DLOG(INFO) << "size " << size_ << " addr length " << (unsigned int)str_addr_length_; return true; } diff --git a/src/base/index_util.cc b/src/base/index_util.cc new file mode 100644 index 00000000000..679ce2deaa7 --- /dev/null +++ b/src/base/index_util.cc @@ -0,0 +1,121 @@ +/* + * Copyright 2021 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "base/index_util.h" + +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "base/glog_wrapper.h" +#include "storage/schema.h" + +namespace openmldb::base { +// , error if empty +std::map> MakePkeysHint(const codec::Schema& schema, + const common::ColumnKey& cidx_ck) { + if (cidx_ck.col_name().empty()) { + LOG(WARNING) << "empty cidx column key"; + return {}; + } + // pkey col idx in row + std::set pkey_set; + for (int i = 0; i < cidx_ck.col_name().size(); i++) { + pkey_set.insert(cidx_ck.col_name().Get(i)); + } + if (pkey_set.empty()) { + LOG(WARNING) << "empty pkey set"; + return {}; + } + if (pkey_set.size() != static_cast::size_type>(cidx_ck.col_name().size())) { + LOG(WARNING) << "pkey set size not equal to cidx pkeys size"; + return {}; + } + std::map> col_idx; + for (int i = 0; i < schema.size(); i++) { + if (pkey_set.find(schema.Get(i).name()) != pkey_set.end()) { + col_idx[schema.Get(i).name()] = {i, schema.Get(i).data_type()}; + } + } + if (col_idx.size() != pkey_set.size()) { + LOG(WARNING) << "col idx size not equal to cidx pkeys size"; + return {}; + } + return col_idx; +} + +// error if empty +std::string MakeDeleteSQL(const std::string& db, const std::string& name, const common::ColumnKey& cidx_ck, + const int8_t* values, uint64_t ts, const codec::RowView& row_view, + const std::map>& col_idx) { + auto sql_prefix = absl::StrCat("delete from ", db, ".", name, " where "); + std::string cond; + for (int i = 0; i < cidx_ck.col_name().size(); i++) { + // append primary keys, pkeys in dimension are encoded, so we should get them from raw value + // split can't work if string has `|` + auto& col_name = cidx_ck.col_name().Get(i); + auto col = col_idx.find(col_name); + if (col == col_idx.end()) { + LOG(WARNING) << "col " << col_name << " not found in col idx"; + return ""; + } + std::string val; + row_view.GetStrValue(values, col->second.first, &val); + if (!cond.empty()) { + absl::StrAppend(&cond, " and "); + } + // TODO(hw): string should add quotes how about timestamp? + // check existence before, so here we skip + absl::StrAppend(&cond, col_name); + if (auto t = col->second.second; t == type::kVarchar || t == type::kString) { + absl::StrAppend(&cond, "=\"", val, "\""); + } else { + absl::StrAppend(&cond, "=", val); + } + } + // ts must be integer, won't be string + if (!cidx_ck.ts_name().empty() && cidx_ck.ts_name() != storage::DEFAULT_TS_COL_NAME) { + if (!cond.empty()) { + absl::StrAppend(&cond, " and "); + } + absl::StrAppend(&cond, cidx_ck.ts_name(), "=", std::to_string(ts)); + } + auto sql = absl::StrCat(sql_prefix, cond, ";"); + // TODO(hw): if delete failed, we can't revert. And if sidx skeys+sts doesn't change, no need to delete and + // then insert + DLOG(INFO) << "delete sql " << sql; + return sql; +} + +// error if empty +std::string ExtractPkeys(const common::ColumnKey& cidx_ck, const int8_t* values, const codec::RowView& row_view, + const std::map>& col_idx) { + // join with | + std::vector pkeys; + for (int i = 0; i < cidx_ck.col_name().size(); i++) { + auto& col_name = cidx_ck.col_name().Get(i); + auto col = col_idx.find(col_name); + if (col == col_idx.end()) { + LOG(WARNING) << "col " << col_name << " not found in col idx"; + return ""; + } + std::string val; + row_view.GetStrValue(values, col->second.first, &val); + pkeys.push_back(val); + } + return absl::StrJoin(pkeys, "|"); +} + +} // namespace openmldb::base diff --git a/src/base/index_util.h b/src/base/index_util.h new file mode 100644 index 00000000000..11392b37bf0 --- /dev/null +++ b/src/base/index_util.h @@ -0,0 +1,45 @@ +/* + * Copyright 2021 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SRC_BASE_INDEX_UTIL_H_ +#define SRC_BASE_INDEX_UTIL_H_ + +#include + +#include "codec/codec.h" + +namespace openmldb { +namespace base { + +// don't declare func in table header cuz swig sdk + +// , error if empty +std::map> MakePkeysHint(const codec::Schema& schema, + const common::ColumnKey& cidx_ck); + +// error if empty +std::string MakeDeleteSQL(const std::string& db, const std::string& name, const common::ColumnKey& cidx_ck, + const int8_t* values, uint64_t ts, const codec::RowView& row_view, + const std::map>& col_idx); + +// error if empty +std::string ExtractPkeys(const common::ColumnKey& cidx_ck, const int8_t* values, const codec::RowView& row_view, + const std::map>& col_idx); + +} // namespace base +} // namespace openmldb + +#endif // SRC_BASE_INDEX_UTIL_H_ diff --git a/src/base/status.h b/src/base/status.h index a2da254e78e..bde6a31960a 100644 --- a/src/base/status.h +++ b/src/base/status.h @@ -191,7 +191,7 @@ enum ReturnCode { }; struct Status { - Status(int code_i, std::string msg_i) : code(code_i), msg(msg_i) {} + Status(int code_i, const std::string& msg_i) : code(code_i), msg(msg_i) {} Status() : code(ReturnCode::kOk), msg("ok") {} inline bool OK() const { return code == ReturnCode::kOk; } inline const std::string& GetMsg() const { return msg; } diff --git a/src/base/status_util.h b/src/base/status_util.h index 1d0db238d61..e0bd5758304 100644 --- a/src/base/status_util.h +++ b/src/base/status_util.h @@ -161,6 +161,13 @@ LOG(WARNING) << "Status: " << _s->ToString(); \ } while (0) +#define APPEND_AND_WARN(s, msg) \ + do { \ + ::hybridse::sdk::Status* _s = (s); \ + _s->Append((msg)); \ + LOG(WARNING) << "Status: " << _s->ToString(); \ + } while (0) + /// @brief s.msg += append_str, and warn it #define CODE_APPEND_AND_WARN(s, code, msg) \ do { \ diff --git a/src/catalog/distribute_iterator.cc b/src/catalog/distribute_iterator.cc index 032d3ec75f2..519dec5f2fa 100644 --- a/src/catalog/distribute_iterator.cc +++ b/src/catalog/distribute_iterator.cc @@ -155,7 +155,7 @@ bool FullTableIterator::NextFromRemote() { } } else { kv_it_ = iter->second->Traverse(tid_, cur_pid_, "", "", 0, FLAGS_traverse_cnt_limit, false, 0, count); - DLOG(INFO) << "count " << count; + DVLOG(1) << "count " << count; } if (kv_it_ && kv_it_->Valid()) { last_pk_ = kv_it_->GetLastPK(); diff --git a/src/client/tablet_client.cc b/src/client/tablet_client.cc index cbfb794817f..635e37d8f78 100644 --- a/src/client/tablet_client.cc +++ b/src/client/tablet_client.cc @@ -164,7 +164,7 @@ base::Status TabletClient::TruncateTable(uint32_t tid, uint32_t pid) { request.set_tid(tid); request.set_pid(pid); if (!client_.SendRequest(&::openmldb::api::TabletServer_Stub::TruncateTable, &request, &response, - FLAGS_request_timeout_ms, 1)) { + FLAGS_request_timeout_ms, 1)) { return {base::ReturnCode::kRPCError, "send request failed!"}; } else if (response.code() == 0) { return {}; @@ -178,7 +178,7 @@ base::Status TabletClient::CreateTable(const ::openmldb::api::TableMeta& table_m table_meta_ptr->CopyFrom(table_meta); ::openmldb::api::CreateTableResponse response; if (!client_.SendRequest(&::openmldb::api::TabletServer_Stub::CreateTable, &request, &response, - FLAGS_request_timeout_ms * 2, 1)) { + FLAGS_request_timeout_ms * 2, 1)) { return {base::ReturnCode::kRPCError, "send request failed!"}; } else if (response.code() == 0) { return {}; @@ -207,9 +207,8 @@ bool TabletClient::UpdateTableMetaForAddField(uint32_t tid, const std::vector>& dimensions, - int memory_usage_limit, bool put_if_absent) { - + const std::vector>& dimensions, int memory_usage_limit, + bool put_if_absent, bool check_exists) { ::google::protobuf::RepeatedPtrField<::openmldb::api::Dimension> pb_dimensions; for (size_t i = 0; i < dimensions.size(); i++) { ::openmldb::api::Dimension* d = pb_dimensions.Add(); @@ -217,12 +216,12 @@ base::Status TabletClient::Put(uint32_t tid, uint32_t pid, uint64_t time, const d->set_idx(dimensions[i].second); } - return Put(tid, pid, time, base::Slice(value), &pb_dimensions, memory_usage_limit, put_if_absent); + return Put(tid, pid, time, base::Slice(value), &pb_dimensions, memory_usage_limit, put_if_absent, check_exists); } base::Status TabletClient::Put(uint32_t tid, uint32_t pid, uint64_t time, const base::Slice& value, - ::google::protobuf::RepeatedPtrField<::openmldb::api::Dimension>* dimensions, - int memory_usage_limit, bool put_if_absent) { + ::google::protobuf::RepeatedPtrField<::openmldb::api::Dimension>* dimensions, + int memory_usage_limit, bool put_if_absent, bool check_exists) { ::openmldb::api::PutRequest request; if (memory_usage_limit < 0 || memory_usage_limit > 100) { return {base::ReturnCode::kError, absl::StrCat("invalid memory_usage_limit ", memory_usage_limit)}; @@ -235,9 +234,10 @@ base::Status TabletClient::Put(uint32_t tid, uint32_t pid, uint64_t time, const request.set_pid(pid); request.mutable_dimensions()->Swap(dimensions); request.set_put_if_absent(put_if_absent); + request.set_check_exists(check_exists); ::openmldb::api::PutResponse response; - auto st = client_.SendRequestSt(&::openmldb::api::TabletServer_Stub::Put, - &request, &response, FLAGS_request_timeout_ms, 1); + auto st = client_.SendRequestSt(&::openmldb::api::TabletServer_Stub::Put, &request, &response, + FLAGS_request_timeout_ms, 1); if (!st.OK()) { return st; } @@ -245,7 +245,7 @@ base::Status TabletClient::Put(uint32_t tid, uint32_t pid, uint64_t time, const } base::Status TabletClient::Put(uint32_t tid, uint32_t pid, const std::string& pk, uint64_t time, - const std::string& value) { + const std::string& value) { ::openmldb::api::PutRequest request; auto dim = request.add_dimensions(); dim->set_key(pk); @@ -255,8 +255,8 @@ base::Status TabletClient::Put(uint32_t tid, uint32_t pid, const std::string& pk request.set_tid(tid); request.set_pid(pid); ::openmldb::api::PutResponse response; - auto st = client_.SendRequestSt(&::openmldb::api::TabletServer_Stub::Put, - &request, &response, FLAGS_request_timeout_ms, 1); + auto st = client_.SendRequestSt(&::openmldb::api::TabletServer_Stub::Put, &request, &response, + FLAGS_request_timeout_ms, 1); if (!st.OK()) { return st; } @@ -369,7 +369,7 @@ base::Status TabletClient::LoadTable(const std::string& name, uint32_t tid, uint } base::Status TabletClient::LoadTableInternal(const ::openmldb::api::TableMeta& table_meta, - std::shared_ptr task_info) { + std::shared_ptr task_info) { ::openmldb::api::LoadTableRequest request; ::openmldb::api::TableMeta* cur_table_meta = request.mutable_table_meta(); cur_table_meta->CopyFrom(table_meta); @@ -524,7 +524,7 @@ bool TabletClient::GetManifest(uint32_t tid, uint32_t pid, ::openmldb::common::S base::Status TabletClient::GetTableStatus(::openmldb::api::GetTableStatusResponse& response) { ::openmldb::api::GetTableStatusRequest request; auto st = client_.SendRequestSt(&::openmldb::api::TabletServer_Stub::GetTableStatus, &request, &response, - FLAGS_request_timeout_ms, 1); + FLAGS_request_timeout_ms, 1); if (st.OK()) { return {response.code(), response.msg()}; } @@ -536,14 +536,14 @@ base::Status TabletClient::GetTableStatus(uint32_t tid, uint32_t pid, ::openmldb } base::Status TabletClient::GetTableStatus(uint32_t tid, uint32_t pid, bool need_schema, - ::openmldb::api::TableStatus& table_status) { + ::openmldb::api::TableStatus& table_status) { ::openmldb::api::GetTableStatusRequest request; request.set_tid(tid); request.set_pid(pid); request.set_need_schema(need_schema); ::openmldb::api::GetTableStatusResponse response; auto st = client_.SendRequestSt(&::openmldb::api::TabletServer_Stub::GetTableStatus, &request, &response, - FLAGS_request_timeout_ms, 1); + FLAGS_request_timeout_ms, 1); if (!st.OK()) { return st; } @@ -553,9 +553,10 @@ base::Status TabletClient::GetTableStatus(uint32_t tid, uint32_t pid, bool need_ return {response.code(), response.msg()}; } -std::shared_ptr TabletClient::Scan(uint32_t tid, uint32_t pid, - const std::string& pk, const std::string& idx_name, - uint64_t stime, uint64_t etime, uint32_t limit, uint32_t skip_record_num, std::string& msg) { +std::shared_ptr TabletClient::Scan(uint32_t tid, uint32_t pid, const std::string& pk, + const std::string& idx_name, uint64_t stime, + uint64_t etime, uint32_t limit, + uint32_t skip_record_num, std::string& msg) { ::openmldb::api::ScanRequest request; request.set_pk(pk); request.set_st(stime); @@ -569,7 +570,7 @@ std::shared_ptr TabletClient::Scan(uint32_t tid, request.set_skip_record_num(skip_record_num); auto response = std::make_shared(); bool ok = client_.SendRequest(&::openmldb::api::TabletServer_Stub::Scan, &request, response.get(), - FLAGS_request_timeout_ms, 1); + FLAGS_request_timeout_ms, 1); if (response->has_msg()) { msg = response->msg(); } @@ -579,9 +580,9 @@ std::shared_ptr TabletClient::Scan(uint32_t tid, return std::make_shared<::openmldb::base::ScanKvIterator>(pk, response); } -std::shared_ptr TabletClient::Scan(uint32_t tid, uint32_t pid, - const std::string& pk, const std::string& idx_name, - uint64_t stime, uint64_t etime, uint32_t limit, std::string& msg) { +std::shared_ptr TabletClient::Scan(uint32_t tid, uint32_t pid, const std::string& pk, + const std::string& idx_name, uint64_t stime, + uint64_t etime, uint32_t limit, std::string& msg) { return Scan(tid, pid, pk, idx_name, stime, etime, limit, 0, msg); } @@ -709,7 +710,7 @@ bool TabletClient::SetExpire(uint32_t tid, uint32_t pid, bool is_expire) { } base::Status TabletClient::GetTableFollower(uint32_t tid, uint32_t pid, uint64_t& offset, - std::map& info_map) { + std::map& info_map) { ::openmldb::api::GetTableFollowerRequest request; ::openmldb::api::GetTableFollowerResponse response; request.set_tid(tid); @@ -799,6 +800,57 @@ bool TabletClient::Get(uint32_t tid, uint32_t pid, const std::string& pk, uint64 return true; } +base::Status TabletClient::Get(uint32_t tid, uint32_t pid, const std::string& pk, uint64_t time, + const std::string& idx_name, std::string& value, uint64_t& ts) { + ::openmldb::api::GetRequest request; + ::openmldb::api::GetResponse response; + request.set_tid(tid); + request.set_pid(pid); + request.set_key(pk); + request.set_ts(time); + if (!idx_name.empty()) { + request.set_idx_name(idx_name); + } + auto st = client_.SendRequestSt(&::openmldb::api::TabletServer_Stub::Get, &request, &response, + FLAGS_request_timeout_ms, 1); + if (!st.OK()) { + return st; + } + + if (response.code() == 0) { + value.swap(*response.mutable_value()); + ts = response.ts(); + } + return {response.code(), response.msg()}; +} + +base::Status TabletClient::Get(uint32_t tid, uint32_t pid, const std::string& pk, uint64_t stime, api::GetType stype, + uint64_t etime, const std::string& idx_name, std::string& value, + uint64_t& ts) { + ::openmldb::api::GetRequest request; + ::openmldb::api::GetResponse response; + request.set_tid(tid); + request.set_pid(pid); + request.set_key(pk); + request.set_ts(stime); + request.set_type(stype); + request.set_et(etime); + if (!idx_name.empty()) { + request.set_idx_name(idx_name); + } + auto st = client_.SendRequestSt(&::openmldb::api::TabletServer_Stub::Get, &request, &response, + FLAGS_request_timeout_ms, 1); + if (!st.OK()) { + return st; + } + + if (response.code() == 0) { + value.swap(*response.mutable_value()); + ts = response.ts(); + } + return {response.code(), response.msg()}; +} + bool TabletClient::Delete(uint32_t tid, uint32_t pid, const std::string& pk, const std::string& idx_name, std::string& msg) { ::openmldb::api::DeleteRequest request; @@ -840,8 +892,7 @@ base::Status TabletClient::Delete(uint32_t tid, uint32_t pid, const sdk::DeleteO request.set_ts_name(option.ts_name); } request.set_enable_decode_value(option.enable_decode_value); - bool ok = client_.SendRequest(&::openmldb::api::TabletServer_Stub::Delete, &request, &response, - timeout_ms, 1); + bool ok = client_.SendRequest(&::openmldb::api::TabletServer_Stub::Delete, &request, &response, timeout_ms, 1); if (!ok || response.code() != 0) { return {base::ReturnCode::kError, response.msg()}; } @@ -885,8 +936,10 @@ bool TabletClient::DeleteBinlog(uint32_t tid, uint32_t pid, openmldb::common::St } std::shared_ptr TabletClient::Traverse(uint32_t tid, uint32_t pid, - const std::string& idx_name, const std::string& pk, uint64_t ts, uint32_t limit, bool skip_current_pk, - uint32_t ts_pos, uint32_t& count) { + const std::string& idx_name, + const std::string& pk, uint64_t ts, + uint32_t limit, bool skip_current_pk, + uint32_t ts_pos, uint32_t& count) { ::openmldb::api::TraverseRequest request; auto response = std::make_shared(); request.set_tid(tid); @@ -966,8 +1019,8 @@ bool TabletClient::AddIndex(uint32_t tid, uint32_t pid, const ::openmldb::common } bool TabletClient::AddMultiIndex(uint32_t tid, uint32_t pid, - const std::vector<::openmldb::common::ColumnKey>& column_keys, - std::shared_ptr task_info) { + const std::vector<::openmldb::common::ColumnKey>& column_keys, + std::shared_ptr task_info) { ::openmldb::api::AddIndexRequest request; ::openmldb::api::GeneralResponse response; request.set_tid(tid); @@ -1039,9 +1092,8 @@ bool TabletClient::LoadIndexData(uint32_t tid, uint32_t pid, uint32_t partition_ } bool TabletClient::ExtractIndexData(uint32_t tid, uint32_t pid, uint32_t partition_num, - const std::vector<::openmldb::common::ColumnKey>& column_key, - uint64_t offset, bool dump_data, - std::shared_ptr task_info) { + const std::vector<::openmldb::common::ColumnKey>& column_key, uint64_t offset, + bool dump_data, std::shared_ptr task_info) { if (column_key.empty()) { if (task_info) { task_info->set_status(::openmldb::api::TaskStatus::kFailed); @@ -1213,7 +1265,7 @@ bool TabletClient::CallSQLBatchRequestProcedure(const std::string& db, const std } bool static ParseBatchRequestMeta(const base::Slice& meta, const base::Slice& data, - ::openmldb::api::SQLBatchRequestQueryRequest* request) { + ::openmldb::api::SQLBatchRequestQueryRequest* request) { uint64_t total_len = 0; const int32_t* buf = reinterpret_cast(meta.data()); int32_t cnt = meta.size() / sizeof(int32_t); @@ -1238,9 +1290,9 @@ bool static ParseBatchRequestMeta(const base::Slice& meta, const base::Slice& da } base::Status TabletClient::CallSQLBatchRequestProcedure(const std::string& db, const std::string& sp_name, - const base::Slice& meta, const base::Slice& data, - bool is_debug, uint64_t timeout_ms, - brpc::Controller* cntl, openmldb::api::SQLBatchRequestQueryResponse* response) { + const base::Slice& meta, const base::Slice& data, bool is_debug, + uint64_t timeout_ms, brpc::Controller* cntl, + openmldb::api::SQLBatchRequestQueryResponse* response) { ::openmldb::api::SQLBatchRequestQueryRequest request; request.set_sp_name(sp_name); request.set_is_procedure(true); @@ -1264,10 +1316,9 @@ base::Status TabletClient::CallSQLBatchRequestProcedure(const std::string& db, c return {}; } -base::Status TabletClient::CallSQLBatchRequestProcedure(const std::string& db, const std::string& sp_name, - const base::Slice& meta, const base::Slice& data, - bool is_debug, uint64_t timeout_ms, - openmldb::RpcCallback* callback) { +base::Status TabletClient::CallSQLBatchRequestProcedure( + const std::string& db, const std::string& sp_name, const base::Slice& meta, const base::Slice& data, bool is_debug, + uint64_t timeout_ms, openmldb::RpcCallback* callback) { if (callback == nullptr) { return {base::ReturnCode::kError, "callback is null"}; } @@ -1286,8 +1337,8 @@ base::Status TabletClient::CallSQLBatchRequestProcedure(const std::string& db, c return {base::ReturnCode::kError, "append to iobuf error"}; } callback->GetController()->set_timeout_ms(timeout_ms); - if (!client_.SendRequest(&::openmldb::api::TabletServer_Stub::SQLBatchRequestQuery, - callback->GetController().get(), &request, callback->GetResponse().get(), callback)) { + if (!client_.SendRequest(&::openmldb::api::TabletServer_Stub::SQLBatchRequestQuery, callback->GetController().get(), + &request, callback->GetResponse().get(), callback)) { return {base::ReturnCode::kError, "stub is null"}; } return {}; @@ -1384,9 +1435,9 @@ bool TabletClient::DropFunction(const ::openmldb::common::ExternalFun& fun, std: return true; } -bool TabletClient::CreateAggregator(const ::openmldb::api::TableMeta& base_table_meta, - uint32_t aggr_tid, uint32_t aggr_pid, uint32_t index_pos, - const ::openmldb::base::LongWindowInfo& window_info) { +bool TabletClient::CreateAggregator(const ::openmldb::api::TableMeta& base_table_meta, uint32_t aggr_tid, + uint32_t aggr_pid, uint32_t index_pos, + const ::openmldb::base::LongWindowInfo& window_info) { ::openmldb::api::CreateAggregatorRequest request; ::openmldb::api::TableMeta* base_meta_ptr = request.mutable_base_table_meta(); base_meta_ptr->CopyFrom(base_table_meta); @@ -1412,7 +1463,7 @@ bool TabletClient::CreateAggregator(const ::openmldb::api::TableMeta& base_table bool TabletClient::GetAndFlushDeployStats(::openmldb::api::DeployStatsResponse* res) { ::openmldb::api::GAFDeployStatsRequest req; bool ok = client_.SendRequest(&::openmldb::api::TabletServer_Stub::GetAndFlushDeployStats, &req, res, - FLAGS_request_timeout_ms, FLAGS_request_max_retry); + FLAGS_request_timeout_ms, FLAGS_request_max_retry); return ok && res->code() == 0; } diff --git a/src/client/tablet_client.h b/src/client/tablet_client.h index 33188adadcc..8099c599d15 100644 --- a/src/client/tablet_client.h +++ b/src/client/tablet_client.h @@ -78,20 +78,24 @@ class TabletClient : public Client { base::Status Put(uint32_t tid, uint32_t pid, const std::string& pk, uint64_t time, const std::string& value); base::Status Put(uint32_t tid, uint32_t pid, uint64_t time, const std::string& value, - const std::vector>& dimensions, - int memory_usage_limit = 0, bool put_if_absent = false); + const std::vector>& dimensions, int memory_usage_limit = 0, + bool put_if_absent = false, bool check_exists = false); base::Status Put(uint32_t tid, uint32_t pid, uint64_t time, const base::Slice& value, - ::google::protobuf::RepeatedPtrField<::openmldb::api::Dimension>* dimensions, - int memory_usage_limit = 0, bool put_if_absent = false); + ::google::protobuf::RepeatedPtrField<::openmldb::api::Dimension>* dimensions, + int memory_usage_limit = 0, bool put_if_absent = false, bool check_exists = false); bool Get(uint32_t tid, uint32_t pid, const std::string& pk, uint64_t time, std::string& value, // NOLINT uint64_t& ts, // NOLINT - std::string& msg); // NOLINT + std::string& msg); // NOLINT bool Get(uint32_t tid, uint32_t pid, const std::string& pk, uint64_t time, const std::string& idx_name, std::string& value, uint64_t& ts, std::string& msg); // NOLINT - + base::Status Get(uint32_t tid, uint32_t pid, const std::string& pk, uint64_t time, const std::string& idx_name, + std::string& value, uint64_t& ts); // NOLINT + base::Status Get(uint32_t tid, uint32_t pid, const std::string& pk, uint64_t stime, api::GetType stype, + uint64_t etime, const std::string& idx_name, std::string& value, + uint64_t& ts); // NOLINT bool Delete(uint32_t tid, uint32_t pid, const std::string& pk, const std::string& idx_name, std::string& msg); // NOLINT diff --git a/src/cmd/display.h b/src/cmd/display.h index 34e1f851e39..0d7d2819964 100644 --- a/src/cmd/display.h +++ b/src/cmd/display.h @@ -105,6 +105,7 @@ __attribute__((unused)) static void PrintColumnKey( t.add("ts"); t.add("ttl"); t.add("ttl_type"); + t.add("type"); t.end_of_row(); int index_pos = 1; for (int i = 0; i < column_key_field.size(); i++) { @@ -141,7 +142,7 @@ __attribute__((unused)) static void PrintColumnKey( t.add("-"); // ttl t.add("-"); // ttl_type } - + t.add(common::IndexType_Name(column_key.type())); t.end_of_row(); } stream << t; diff --git a/src/cmd/sql_cmd_test.cc b/src/cmd/sql_cmd_test.cc index 79225eb52dd..0b29ae449cd 100644 --- a/src/cmd/sql_cmd_test.cc +++ b/src/cmd/sql_cmd_test.cc @@ -1284,7 +1284,7 @@ TEST_P(DBSDKTest, Truncate) { sr->ExecuteSQL(absl::StrCat("insert into ", table_name, " values ('", key, "', 11, ", ts, ");"), &status); } } - absl::SleepFor(absl::Seconds(5)); + absl::SleepFor(absl::Seconds(16)); // sleep more to avoid truncate failed on partition offset mismatch res = sr->ExecuteSQL(absl::StrCat("select * from ", table_name, ";"), &status); ASSERT_EQ(res->Size(), 100); @@ -1556,18 +1556,18 @@ TEST_P(DBSDKTest, SQLDeletetRow) { res = sr->ExecuteSQL(absl::StrCat("select * from ", table_name, ";"), &status); ASSERT_EQ(res->Size(), 3); std::string delete_sql = "delete from " + table_name + " where c1 = ?;"; - auto insert_row = sr->GetDeleteRow(db_name, delete_sql, &status); + auto delete_row = sr->GetDeleteRow(db_name, delete_sql, &status); ASSERT_TRUE(status.IsOK()); - insert_row->SetString(1, "key3"); - ASSERT_TRUE(insert_row->Build()); - sr->ExecuteDelete(insert_row, &status); + delete_row->SetString(1, "key3"); + ASSERT_TRUE(delete_row->Build()); + sr->ExecuteDelete(delete_row, &status); ASSERT_TRUE(status.IsOK()); res = sr->ExecuteSQL(absl::StrCat("select * from ", table_name, ";"), &status); ASSERT_EQ(res->Size(), 2); - insert_row->Reset(); - insert_row->SetString(1, "key100"); - ASSERT_TRUE(insert_row->Build()); - sr->ExecuteDelete(insert_row, &status); + delete_row->Reset(); + delete_row->SetString(1, "key100"); + ASSERT_TRUE(delete_row->Build()); + sr->ExecuteDelete(delete_row, &status); ASSERT_TRUE(status.IsOK()); res = sr->ExecuteSQL(absl::StrCat("select * from ", table_name, ";"), &status); ASSERT_EQ(res->Size(), 2); diff --git a/src/codec/field_codec.h b/src/codec/field_codec.h index 452578ff9fc..14f5ec14a5a 100644 --- a/src/codec/field_codec.h +++ b/src/codec/field_codec.h @@ -35,8 +35,8 @@ namespace codec { template static bool AppendColumnValue(const std::string& v, hybridse::sdk::DataType type, bool is_not_null, const std::string& null_value, T row) { - // check if null - if (v == null_value) { + // check if null, empty string will cast fail and throw bad_lexical_cast + if (v.empty() || v == null_value) { if (is_not_null) { return false; } diff --git a/src/flags.cc b/src/flags.cc index 2a061dbd263..744da1dac64 100644 --- a/src/flags.cc +++ b/src/flags.cc @@ -187,3 +187,7 @@ DEFINE_int32(sync_job_timeout, 30 * 60 * 1000, "sync job timeout, unit is milliseconds, should <= server.channel_keep_alive_time in TaskManager"); DEFINE_int32(deploy_job_max_wait_time_ms, 30 * 60 * 1000, "the max wait time of waiting deploy job"); DEFINE_bool(skip_grant_tables, true, "skip the grant tables"); + +// iot +// not exactly size, may plus some TODO(hw): too small? +DEFINE_uint32(cidx_gc_max_size, 1000, "config the max size for one cidx segment gc"); diff --git a/src/nameserver/name_server_impl.cc b/src/nameserver/name_server_impl.cc index 5e65a7d2d94..6aabec47d1f 100644 --- a/src/nameserver/name_server_impl.cc +++ b/src/nameserver/name_server_impl.cc @@ -1564,8 +1564,7 @@ bool NameServerImpl::Init(const std::string& zk_cluster, const std::string& zk_p task_thread_pool_.DelayTask(FLAGS_make_snapshot_check_interval, boost::bind(&NameServerImpl::SchedMakeSnapshot, this)); std::shared_ptr<::openmldb::nameserver::TableInfo> table_info; - while ( - !GetTableInfo(::openmldb::nameserver::USER_INFO_NAME, ::openmldb::nameserver::INTERNAL_DB, &table_info)) { + while (!GetTableInfo(::openmldb::nameserver::USER_INFO_NAME, ::openmldb::nameserver::INTERNAL_DB, &table_info)) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } return true; @@ -3818,6 +3817,8 @@ void NameServerImpl::CreateTable(RpcController* controller, const CreateTableReq table_info->set_partition_num(1); table_info->set_replica_num(1); } + // TODO(hw): valid index pattern 1. all covering 2. clustered + secondary/covering(only one clustered and it should + // be the first one) auto status = schema::SchemaAdapter::CheckTableMeta(*table_info); if (!status.OK()) { PDLOG(WARNING, status.msg.c_str()); @@ -8675,6 +8676,11 @@ void NameServerImpl::AddIndex(RpcController* controller, const AddIndexRequest* std::vector<::openmldb::common::ColumnKey> column_key_vec; if (request->column_keys_size() > 0) { for (const auto& column_key : request->column_keys()) { + if (column_key.type() == common::IndexType::kClustered) { + base::SetResponseStatus(ReturnCode::kWrongColumnKey, "add clustered index is not allowed", response); + LOG(WARNING) << "add clustered index is not allowed"; + return; + } column_key_vec.push_back(column_key); } } else { @@ -9530,8 +9536,8 @@ base::Status NameServerImpl::CreateProcedureOnTablet(const ::openmldb::api::Crea ", endpoint: ", tb_client->GetEndpoint(), ", msg: ", status.GetMsg())}; } DLOG(INFO) << "create procedure on tablet success. db_name: " << sp_info.db_name() << ", " - << "sp_name: " << sp_info.sp_name() << ", " << "sql: " << sp_info.sql() - << "endpoint: " << tb_client->GetEndpoint(); + << "sp_name: " << sp_info.sp_name() << ", " + << "sql: " << sp_info.sql() << "endpoint: " << tb_client->GetEndpoint(); } return {}; } @@ -10120,11 +10126,7 @@ void NameServerImpl::ShowFunction(RpcController* controller, const ShowFunctionR base::Status NameServerImpl::InitGlobalVarTable() { std::map default_value = { - {"execute_mode", "online"}, - {"enable_trace", "false"}, - {"sync_job", "false"}, - {"job_timeout", "20000"} - }; + {"execute_mode", "online"}, {"enable_trace", "false"}, {"sync_job", "false"}, {"job_timeout", "20000"}}; // get table_info std::string db = INFORMATION_SCHEMA_DB; std::string table = GLOBAL_VARIABLES; diff --git a/src/proto/common.proto b/src/proto/common.proto index 8241e646f34..ee4c23e1c68 100755 --- a/src/proto/common.proto +++ b/src/proto/common.proto @@ -64,12 +64,19 @@ message TTLSt { optional uint64 lat_ttl = 3 [default = 0]; } +enum IndexType { + kCovering = 0; + kClustered = 1; + kSecondary = 2; +} + message ColumnKey { optional string index_name = 1; repeated string col_name = 2; optional string ts_name = 3; optional uint32 flag = 4 [default = 0]; // 0 mean index exist, 1 mean index has been deleted optional TTLSt ttl = 5; + optional IndexType type = 6 [default = kCovering]; } message EndpointAndTid { diff --git a/src/proto/tablet.proto b/src/proto/tablet.proto index bc160a01f1e..253eb35b33e 100755 --- a/src/proto/tablet.proto +++ b/src/proto/tablet.proto @@ -196,6 +196,7 @@ message PutRequest { optional uint32 format_version = 8 [default = 0, deprecated = true]; optional uint32 memory_limit = 9; optional bool put_if_absent = 10 [default = false]; + optional bool check_exists = 11 [default = false]; } message PutResponse { diff --git a/src/sdk/node_adapter.cc b/src/sdk/node_adapter.cc index c7c0d191922..d6b3979cfe7 100644 --- a/src/sdk/node_adapter.cc +++ b/src/sdk/node_adapter.cc @@ -383,6 +383,7 @@ bool NodeAdapter::TransformToTableDef(::hybridse::node::CreatePlanNode* create_n if (!TransformToColumnKey(column_index, column_names, index, status)) { return false; } + DLOG(INFO) << "index column key [" << index->ShortDebugString() << "]"; break; } @@ -471,6 +472,12 @@ bool NodeAdapter::TransformToColumnKey(hybridse::node::ColumnIndexNode* column_i for (const auto& key : column_index->GetKey()) { index->add_col_name(key); } + auto& type = column_index->GetIndexType(); + if (type == "skey") { + index->set_type(common::IndexType::kSecondary); + } else if (type == "ckey") { + index->set_type(common::IndexType::kClustered); + } // else default type kCovering // if no column_names, skip check if (!column_names.empty()) { for (const auto& col : index->col_name()) { diff --git a/src/sdk/option.h b/src/sdk/option.h index 3acb4e30afa..ae6fef7dfac 100644 --- a/src/sdk/option.h +++ b/src/sdk/option.h @@ -17,16 +17,26 @@ #ifndef SRC_SDK_OPTION_H_ #define SRC_SDK_OPTION_H_ +#include #include +#include "absl/strings/str_cat.h" + namespace openmldb { namespace sdk { struct DeleteOption { DeleteOption(std::optional idx_i, const std::string& key_i, const std::string& ts_name_i, - std::optional start_ts_i, std::optional end_ts_i) : - idx(idx_i), key(key_i), ts_name(ts_name_i), start_ts(start_ts_i), end_ts(end_ts_i) {} + std::optional start_ts_i, std::optional end_ts_i) + : idx(idx_i), key(key_i), ts_name(ts_name_i), start_ts(start_ts_i), end_ts(end_ts_i) {} DeleteOption() = default; + std::string DebugString() { + return absl::StrCat("idx: ", idx.has_value() ? std::to_string(idx.value()) : "-1", ", key: ", key, + ", ts_name: ", ts_name, + ", start_ts: ", start_ts.has_value() ? std::to_string(start_ts.value()) : "-1", + ", end_ts: ", end_ts.has_value() ? std::to_string(end_ts.value()) : "-1", + ", enable_decode_value: ", enable_decode_value ? "true" : "false"); + } std::optional idx = std::nullopt; std::string key; std::string ts_name; diff --git a/src/sdk/sql_cluster_router.cc b/src/sdk/sql_cluster_router.cc index 3d09156fdcc..8ef74f8ac2d 100644 --- a/src/sdk/sql_cluster_router.cc +++ b/src/sdk/sql_cluster_router.cc @@ -37,6 +37,7 @@ #include "base/file_util.h" #include "base/glog_wrapper.h" #include "base/status_util.h" +#include "base/index_util.h" #include "boost/none.hpp" #include "boost/property_tree/ini_parser.hpp" #include "boost/property_tree/ptree.hpp" @@ -63,6 +64,7 @@ #include "sdk/result_set_sql.h" #include "sdk/sdk_util.h" #include "sdk/split.h" +#include "storage/index_organized_table.h" #include "udf/udf.h" #include "vm/catalog.h" #include "vm/engine.h" @@ -1284,12 +1286,11 @@ bool SQLClusterRouter::ExecuteInsert(const std::string& db, const std::string& s std::vector fails; if (!codegen_rows.empty()) { - for (size_t i = 0 ; i < codegen_rows.size(); ++i) { + for (size_t i = 0; i < codegen_rows.size(); ++i) { auto r = codegen_rows[i]; auto row = std::make_shared(table_info, schema, r, put_if_absent); if (!PutRow(table_info->tid(), row, tablets, status)) { - LOG(WARNING) << "fail to put row[" - << "] due to: " << status->msg; + LOG(WARNING) << "fail to put row[" << i << "] due to: " << status->msg; fails.push_back(i); continue; } @@ -1323,12 +1324,156 @@ bool SQLClusterRouter::ExecuteInsert(const std::string& db, const std::string& s return true; } +bool IsIOT(const nameserver::TableInfo& table_info) { + auto& cks = table_info.column_key(); + if (cks.empty()) { + LOG(WARNING) << "no index in meta"; + return false; + } + if (cks[0].has_type() && cks[0].type() == common::IndexType::kClustered) { + // check other indexes + for (int i = 1; i < cks.size(); i++) { + if (cks[i].has_type() && cks[i].type() == common::IndexType::kClustered) { + LOG(WARNING) << "should be only one clustered index"; + return false; + } + } + return true; + } + return false; +} + +// clustered index idx must be 0 +bool IsClusteredIndexIdx(const openmldb::api::Dimension& dim_index) { return dim_index.idx() == 0; } +bool IsClusteredIndexIdx(const std::pair& dim_index) { return dim_index.second == 0; } + +std::string ClusteredIndexTsName(const nameserver::TableInfo& table_info) { + auto& cks = table_info.column_key(); + if (cks.empty()) { + LOG(WARNING) << "no index in meta"; + return ""; + } + if (cks[0].has_ts_name() && cks[0].ts_name() != storage::DEFAULT_TS_COL_NAME) { + return cks[0].ts_name(); + } + // if default ts col, return empty string + return ""; +} + bool SQLClusterRouter::PutRow(uint32_t tid, const std::shared_ptr& row, const std::vector>& tablets, ::hybridse::sdk::Status* status) { RET_FALSE_IF_NULL_AND_WARN(status, "output status is nullptr"); const auto& dimensions = row->GetDimensions(); uint64_t cur_ts = ::baidu::common::timer::get_micros() / 1000; + // if iot, check if primary key exists in cidx + if (IsIOT(row->GetTableInfo())) { + if (row->IsPutIfAbsent()) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "put_if_absent is not supported for iot table"); + return false; + } + // dimensions map>, find the idxid == 0 + bool valid = false; + uint64_t ts = 0; + std::string exists_value; // if empty, no primary key exists + + auto cols = row->GetTableInfo().column_desc(); // copy + codec::RowView row_view(cols); + // get cidx pid tablet for existence check + for (const auto& kv : dimensions) { + uint32_t pid = kv.first; + for (auto& pair : kv.second) { + if (IsClusteredIndexIdx(pair)) { + // check if primary key exists on tablet + auto tablet = tablets[pid]; + if (!tablet) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, + "tablet accessor is nullptr, can't check clustered index"); + return false; + } + auto client = tablet->GetClient(); + if (!client) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, + "tablet client is nullptr, can't check clustered index"); + return false; + } + int64_t get_ts = 0; + auto ts_name = ClusteredIndexTsName(row->GetTableInfo()); + if (!ts_name.empty()) { + bool found = false; + for (int i = 0; i < cols.size(); i++) { + if (cols.Get(i).name() == ts_name) { + row_view.GetInteger(reinterpret_cast(row->GetRow().c_str()), i, + cols.Get(i).data_type(), &get_ts); + found = true; + break; + } + } + if (!found || get_ts < 0) { + SET_STATUS_AND_WARN( + status, StatusCode::kCmdError, + found ? "invalid ts " + std::to_string(get_ts) : "get ts column failed"); + return false; + } + } else { + DLOG(INFO) << "no ts column in cidx"; + } + // if get_ts == 0, cidx may be without ts column, you should check ts col in cidx info, not by + // get_ts + DLOG(INFO) << "get primary key on iot table, pid " << pid << ", key " << pair.first << ", ts " + << get_ts; + // get rpc can't read all data(expired data may still in data skiplist), so we use put to check + // exists only check in cidx, no real insertion. get_ts may not be the current time, it can be ts + // col value, it's a bit different. + auto st = client->Put(tid, pid, get_ts, row->GetRow(), {pair}, + insert_memory_usage_limit_.load(std::memory_order_relaxed), false, true); + if (!st.OK() && st.GetCode() != base::ReturnCode::kKeyNotFound) { + APPEND_FROM_BASE_AND_WARN(status, st, "get primary key failed"); + return false; + } + valid = true; + DLOG(INFO) << "Get result: " << st.ToString(); + // check result, won't set exists_value if key not found + if (st.OK()) { + DLOG(INFO) << "primary key exists on iot table"; + exists_value = row->GetRow(); + ts = get_ts; + } + } + } + } + if (!valid) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, + "can't check primary key on iot table, meta/connection error"); + return false; + } + DLOG_IF(INFO, exists_value.empty()) << "primary key not exists, safe to insert"; + if (!exists_value.empty()) { + // delete old data then insert new data, no concurrency control, be careful + // revertput or SQLDeleteRow is not easy to use here, so make a sql + DLOG(INFO) << "primary key exists, delete old data then insert new data"; + // just where primary key, not all columns(redundant condition) + auto hint = base::MakePkeysHint(row->GetTableInfo().column_desc(), + row->GetTableInfo().column_key(0)); + if (hint.empty()) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "make pkeys hint failed"); + return false; + } + auto sql = base::MakeDeleteSQL(row->GetTableInfo().db(), row->GetTableInfo().name(), + row->GetTableInfo().column_key(0), + (int8_t*)exists_value.c_str(), ts, row_view, hint); + if (sql.empty()) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "make delete sql failed"); + return false; + } + ExecuteSQL(sql, status); + if (status->code != 0) { + PREPEND_AND_WARN(status, "delete old data failed"); + return false; + } + DLOG(INFO) << "delete old data success"; + } + } for (const auto& kv : dimensions) { uint32_t pid = kv.first; if (pid < tablets.size()) { @@ -1342,16 +1487,17 @@ bool SQLClusterRouter::PutRow(uint32_t tid, const std::shared_ptr& client->Put(tid, pid, cur_ts, row->GetRow(), kv.second, insert_memory_usage_limit_.load(std::memory_order_relaxed), row->IsPutIfAbsent()); if (!ret.OK()) { - if (RevertPut(row->GetTableInfo(), pid, dimensions, cur_ts, base::Slice(row->GetRow()), tablets) - .IsOK()) { - SET_STATUS_AND_WARN(status, StatusCode::kCmdError, - absl::StrCat("INSERT failed, tid ", tid)); + APPEND_FROM_BASE(status, ret, "put failed"); + if (auto rp = RevertPut(row->GetTableInfo(), pid, dimensions, cur_ts, + base::Slice(row->GetRow()), tablets); + rp.IsOK()) { + APPEND_AND_WARN(status, "tid " + std::to_string(tid) + ". RevertPut success."); } else { - SET_STATUS_AND_WARN(status, StatusCode::kCmdError, - "INSERT failed, tid " + std::to_string(tid) + - ". Note that data might have been partially inserted. " - "You are encouraged to perform DELETE to remove any partially " - "inserted data before trying INSERT again."); + APPEND_AND_WARN(status, "tid " + std::to_string(tid) + + ". RevertPut failed: " + rp.ToString() + + "Note that data might have been partially inserted. " + "You are encouraged to perform DELETE to remove any " + "partially inserted data before trying INSERT again."); } return false; } @@ -1431,7 +1577,7 @@ bool SQLClusterRouter::ExecuteInsert(const std::string& db, const std::string& n std::vector> tablets; bool ret = cluster_sdk_->GetTablet(db, name, &tablets); if (!ret || tablets.empty()) { - status->msg = "fail to get table " + name + " tablet"; + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "fail to get table " + name + " tablet"); return false; } std::map> dimensions_map; @@ -1454,6 +1600,114 @@ bool SQLClusterRouter::ExecuteInsert(const std::string& db, const std::string& n } base::Slice row_value(value, len); uint64_t cur_ts = ::baidu::common::timer::get_micros() / 1000; + // TODO(hw): refactor with PutRow + // if iot, check if primary key exists in cidx + auto table_info = cluster_sdk_->GetTableInfo(db, name); + if (!table_info) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "fail to get table info"); + return false; + } + if (IsIOT(*table_info)) { + if (put_if_absent) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "put_if_absent is not supported for iot table"); + return false; + } + // dimensions map>, find the idxid == 0 + bool valid = false; + uint64_t ts = 0; + std::string exists_value; // if empty, no primary key exists + // TODO: ref putrow, fix later + auto cols = table_info->column_desc(); // copy + codec::RowView row_view(cols); + for (const auto& kv : dimensions_map) { + uint32_t pid = kv.first; + for (auto& pair : kv.second) { + if (IsClusteredIndexIdx(pair)) { + // check if primary key exists on tablet + auto tablet = tablets[pid]; + if (!tablet) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, + "tablet accessor is nullptr, can't check clustered index"); + return false; + } + auto client = tablet->GetClient(); + if (!client) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, + "tablet client is nullptr, can't check clustered index"); + return false; + } + int64_t get_ts = 0; + if (auto ts_name = ClusteredIndexTsName(*table_info); !ts_name.empty()) { + bool found = false; + for (int i = 0; i < cols.size(); i++) { + if (cols.Get(i).name() == ts_name) { + row_view.GetInteger(reinterpret_cast(value), i, cols.Get(i).data_type(), + &get_ts); + found = true; + break; + } + } + if (!found || get_ts < 0) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, + found ? "invalid ts" + std::to_string(get_ts) : "get ts column failed"); + return false; + } + } else { + DLOG(INFO) << "no ts column in cidx"; + } + // if get_ts == 0, may be cidx without ts column + DLOG(INFO) << "get key " << pair.key() << ", ts " << get_ts; + ::google::protobuf::RepeatedPtrField<::openmldb::api::Dimension> dims; + dims.Add()->CopyFrom(pair); + auto st = client->Put(tid, pid, get_ts, row_value, &dims, + insert_memory_usage_limit_.load(std::memory_order_relaxed), false, true); + if (!st.OK() && st.GetCode() != base::ReturnCode::kKeyNotFound) { + APPEND_FROM_BASE_AND_WARN(status, st, "get primary key failed"); + return false; + } + valid = true; + DLOG(INFO) << "Get result: " << st.ToString(); + // check result, won't set exists_value if key not found + if (st.OK()) { + DLOG(INFO) << "primary key exist on iot table"; + exists_value = value; + ts = get_ts; + } + } + } + } + if (!valid) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, + "can't check primary key on iot table, meta/connection error"); + return false; + } + DLOG_IF(INFO, exists_value.empty()) << "primary key not exists, safe to insert"; + if (!exists_value.empty()) { + // delete old data then insert new data, no concurrency control, be careful + // revertput or SQLDeleteRow is not easy to use here, so make a sql? + DLOG(INFO) << "primary key exists, delete old data then insert new data"; + // just where primary key, not all columns(redundant condition) + auto hint = + base::MakePkeysHint(table_info->column_desc(), table_info->column_key(0)); + if (hint.empty()) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "make pkeys hint failed"); + return false; + } + auto sql = base::MakeDeleteSQL(table_info->db(), table_info->name(), + table_info->column_key(0), + (int8_t*)exists_value.c_str(), ts, row_view, hint); + if (sql.empty()) { + SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "make delete sql failed"); + return false; + } + ExecuteSQL(sql, status); + if (status->code != 0) { + PREPEND_AND_WARN(status, "delete old data failed"); + return false; + } + } + } + for (auto& kv : dimensions_map) { uint32_t pid = kv.first; if (pid < tablets.size()) { @@ -1461,17 +1715,13 @@ bool SQLClusterRouter::ExecuteInsert(const std::string& db, const std::string& n if (tablet) { auto client = tablet->GetClient(); if (client) { - DLOG(INFO) << "put data to endpoint " << client->GetEndpoint() << " with dimensions size " - << kv.second.size(); + DVLOG(3) << "put data to endpoint " << client->GetEndpoint() << " with dimensions size " + << kv.second.size(); auto ret = client->Put(tid, pid, cur_ts, row_value, &kv.second, insert_memory_usage_limit_.load(std::memory_order_relaxed), put_if_absent); if (!ret.OK()) { // TODO(hw): show put failed row(readable)? ::hybridse::codec::RowView::GetRowString? - SET_STATUS_AND_WARN(status, StatusCode::kCmdError, - "INSERT failed, tid " + std::to_string(tid) + - ". Note that data might have been partially inserted. " - "You are encouraged to perform DELETE to remove any partially " - "inserted data before trying INSERT again."); + APPEND_FROM_BASE(status, ret, "INSERT failed, tid " + std::to_string(tid)); std::map>> dimensions; for (const auto& val : dimensions_map) { std::vector> vec; @@ -1480,14 +1730,14 @@ bool SQLClusterRouter::ExecuteInsert(const std::string& db, const std::string& n } dimensions.emplace(val.first, std::move(vec)); } - auto table_info = cluster_sdk_->GetTableInfo(db, name); - if (!table_info) { - return false; - } + // TODO(hw): better to return absl::Status - if (RevertPut(*table_info, pid, dimensions, cur_ts, row_value, tablets).IsOK()) { - SET_STATUS_AND_WARN(status, StatusCode::kCmdError, - absl::StrCat("INSERT failed, tid ", tid)); + if (auto rp = RevertPut(*table_info, pid, dimensions, cur_ts, row_value, tablets); rp.IsOK()) { + APPEND_AND_WARN(status, "revert ok"); + } else { + APPEND_AND_WARN(status, + "revert failed. You are encouraged to perform DELETE to remove any " + "partially inserted data before trying INSERT again."); } return false; } @@ -1699,7 +1949,7 @@ std::shared_ptr SQLClusterRouter::HandleSQLCmd(const h } case hybridse::node::kCmdShowUser: { - std::vector value = { options_->user }; + std::vector value = {options_->user}; return ResultSetSQL::MakeResultSet({"User"}, {value}, status); } @@ -2120,6 +2370,32 @@ std::shared_ptr SQLClusterRouter::HandleSQLCmd(const h return {}; } +base::Status ValidateTableInfo(const nameserver::TableInfo& table_info) { + auto& indexs = table_info.column_key(); + if (indexs.empty()) { + LOG(INFO) << "no index specified, it'll add default index later"; + return {}; + } + if (indexs[0].type() == common::IndexType::kCovering) { + // MemTable, all other indexs should be covering + for (int i = 1; i < indexs.size(); i++) { + if (indexs[i].type() != common::IndexType::kCovering) { + return {base::ReturnCode::kInvalidArgs, "index " + std::to_string(i) + " should be covering"}; + } + } + } else if (indexs[0].type() == common::IndexType::kClustered) { + // IOT, no more clustered index, secondary and covering are valid + for (int i = 1; i < indexs.size(); i++) { + if (indexs[i].type() == common::IndexType::kClustered) { + return {base::ReturnCode::kInvalidArgs, "index " + std::to_string(i) + " should not be clustered"}; + } + } + } else { + return {base::ReturnCode::kInvalidArgs, "index 0 should be clustered or covering"}; + } + return {}; +} + base::Status SQLClusterRouter::HandleSQLCreateTable(hybridse::node::CreatePlanNode* create_node, const std::string& db, std::shared_ptr<::openmldb::client::NsClient> ns_ptr) { return HandleSQLCreateTable(create_node, db, ns_ptr, ""); @@ -2148,11 +2424,16 @@ base::Status SQLClusterRouter::HandleSQLCreateTable(hybridse::node::CreatePlanNo hybridse::base::Status sql_status; bool is_cluster_mode = cluster_sdk_->IsClusterMode(); + ::openmldb::sdk::NodeAdapter::TransformToTableDef(create_node, &table_info, default_replica_num, is_cluster_mode, &sql_status); if (sql_status.code != 0) { return base::Status(sql_status.code, sql_status.msg); } + // clustered should be the first index, user should set it, we don't adjust it + if (auto st = ValidateTableInfo(table_info); !st.OK()) { + return st; + } std::string msg; if (!ns_ptr->CreateTable(table_info, create_node->GetIfNotExist(), msg)) { return base::Status(base::ReturnCode::kSQLCmdRunError, msg); @@ -2764,7 +3045,8 @@ std::shared_ptr SQLClusterRouter::ExecuteSQL( } case hybridse::node::kPlanTypeCreateUser: { auto create_node = dynamic_cast(node); - UserInfo user_info;; + UserInfo user_info; + auto result = GetUser(create_node->Name(), &user_info); if (!result.ok()) { *status = {StatusCode::kCmdError, result.status().message()}; @@ -3005,7 +3287,7 @@ std::shared_ptr SQLClusterRouter::ExecuteSQL( if (is_online_mode) { // Handle in online mode config.emplace("spark.insert_memory_usage_limit", - std::to_string(insert_memory_usage_limit_.load(std::memory_order_relaxed))); + std::to_string(insert_memory_usage_limit_.load(std::memory_order_relaxed))); base_status = ImportOnlineData(sql, config, database, is_sync_job, offline_job_timeout, &job_info); } else { // Handle in offline mode @@ -3540,10 +3822,11 @@ hybridse::sdk::Status SQLClusterRouter::LoadDataMultipleFile(int id, int step, c const std::vector& file_list, const openmldb::sdk::LoadOptionsMapParser& options_parser, uint64_t* count) { + *count = 0; for (const auto& file : file_list) { uint64_t cur_count = 0; auto status = LoadDataSingleFile(id, step, database, table, file, options_parser, &cur_count); - DLOG(INFO) << "[thread " << id << "] Loaded " << count << " rows in " << file; + DLOG(INFO) << "[thread " << id << "] Loaded " << cur_count << " rows in " << file; if (!status.IsOK()) { return status; } @@ -3712,6 +3995,7 @@ hybridse::sdk::Status SQLClusterRouter::HandleDelete(const std::string& db, cons if (!status.IsOK()) { return status; } + DLOG(INFO) << "delete option: " << option.DebugString(); status = SendDeleteRequst(table_info, option); if (status.IsOK() && db != nameserver::INTERNAL_DB) { status = { @@ -4959,10 +5243,10 @@ std::shared_ptr SQLClusterRouter::GetNameServerJobResu } absl::StatusOr SQLClusterRouter::GetUser(const std::string& name, UserInfo* user_info) { - std::string sql = absl::StrCat("select * from ", nameserver::USER_INFO_NAME); + std::string sql = absl::StrCat("select * from ", nameserver::USER_INFO_NAME); hybridse::sdk::Status status; - auto rs = ExecuteSQLParameterized(nameserver::INTERNAL_DB, sql, - std::shared_ptr(), &status); + auto rs = + ExecuteSQLParameterized(nameserver::INTERNAL_DB, sql, std::shared_ptr(), &status); if (rs == nullptr) { return absl::InternalError(status.msg); } @@ -5014,6 +5298,8 @@ hybridse::sdk::Status SQLClusterRouter::UpdateUser(const UserInfo& user_info, co } hybridse::sdk::Status SQLClusterRouter::DeleteUser(const std::string& name) { + std::string sql = + absl::StrCat("delete from ", nameserver::USER_INFO_NAME, " where host = '%' and user = '", name, "';"); hybridse::sdk::Status status; auto ns_client = cluster_sdk_->GetNsClient(); @@ -5035,12 +5321,10 @@ void SQLClusterRouter::AddUserToConfig(std::map* confi } } -::hybridse::sdk::Status SQLClusterRouter::RevertPut(const nameserver::TableInfo& table_info, - uint32_t end_pid, - const std::map>>& dimensions, - uint64_t ts, - const base::Slice& value, - const std::vector>& tablets) { +::hybridse::sdk::Status SQLClusterRouter::RevertPut( + const nameserver::TableInfo& table_info, uint32_t end_pid, + const std::map>>& dimensions, uint64_t ts, + const base::Slice& value, const std::vector>& tablets) { codec::RowView row_view(table_info.column_desc()); std::map column_map; for (int32_t i = 0; i < table_info.column_desc_size(); i++) { diff --git a/src/storage/index_organized_table.cc b/src/storage/index_organized_table.cc new file mode 100644 index 00000000000..aeb3302b22b --- /dev/null +++ b/src/storage/index_organized_table.cc @@ -0,0 +1,634 @@ +/* + * Copyright 2021 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "storage/index_organized_table.h" + +#include + +#include "absl/strings/str_join.h" // dlog +#include "absl/strings/str_split.h" +#include "sdk/sql_router.h" +#include "storage/iot_segment.h" +#include "base/index_util.h" + +DECLARE_uint32(absolute_default_skiplist_height); + +namespace openmldb::storage { + +IOTIterator* NewNullIterator() { + // if TimeEntries::Iterator is null, nothing will be used + return new IOTIterator(nullptr, type::CompressType::kNoCompress, {}); +} + +// TODO(hw): temp func to create iot iterator +IOTIterator* NewIOTIterator(Segment* segment, const Slice& key, Ticket& ticket, type::CompressType compress_type, + std::unique_ptr cidx_iter) { + void* entry = nullptr; + auto entries = segment->GetKeyEntries(); + if (entries == nullptr || segment->GetTsCnt() > 1 || entries->Get(key, entry) < 0 || entry == nullptr) { + return NewNullIterator(); + } + ticket.Push(reinterpret_cast(entry)); + return new IOTIterator(reinterpret_cast(entry)->entries.NewIterator(), compress_type, + std::move(cidx_iter)); +} + +IOTIterator* NewIOTIterator(Segment* segment, const Slice& key, uint32_t idx, Ticket& ticket, + type::CompressType compress_type, + std::unique_ptr cidx_iter) { + auto ts_idx_map = segment->GetTsIdxMap(); + auto pos = ts_idx_map.find(idx); + if (pos == ts_idx_map.end()) { + LOG(WARNING) << "can't find idx in segment"; + return NewNullIterator(); + } + auto entries = segment->GetKeyEntries(); + if (segment->GetTsCnt() == 1) { + return NewIOTIterator(segment, key, ticket, compress_type, std::move(cidx_iter)); + } + void* entry_arr = nullptr; + if (entries->Get(key, entry_arr) < 0 || entry_arr == nullptr) { + return NewNullIterator(); + } + auto entry = reinterpret_cast(entry_arr)[pos->second]; + ticket.Push(entry); + return new IOTIterator(entry->entries.NewIterator(), compress_type, std::move(cidx_iter)); +} + +TableIterator* IndexOrganizedTable::NewIterator(uint32_t index, const std::string& pk, Ticket& ticket) { + std::shared_ptr index_def = table_index_.GetIndex(index); + if (!index_def || !index_def->IsReady()) { + LOG(WARNING) << "index is invalid"; + return nullptr; + } + DLOG(INFO) << "new iter for index and pk " << index << " name " << index_def->GetName(); + uint32_t seg_idx = SegIdx(pk); + Slice spk(pk); + uint32_t real_idx = index_def->GetInnerPos(); + Segment* segment = GetSegment(real_idx, seg_idx); + auto ts_col = index_def->GetTsColumn(); + if (ts_col) { + // if secondary, use iot iterator + if (index_def->IsSecondaryIndex()) { + // get clustered index iter for secondary index + auto handler = catalog_->GetTable(GetDB(), GetName()); + if (!handler) { + LOG(WARNING) << "no TableHandler for " << GetDB() << "." << GetName(); + return nullptr; + } + auto tablet_table_handler = std::dynamic_pointer_cast(handler); + if (!tablet_table_handler) { + LOG(WARNING) << "convert TabletTableHandler failed for " << GetDB() << "." << GetName(); + return nullptr; + } + LOG(INFO) << "create iot iterator for pk"; + // TODO(hw): iter may be invalid if catalog updated + auto iter = + NewIOTIterator(segment, spk, ts_col->GetId(), ticket, GetCompressType(), + std::move(tablet_table_handler->GetWindowIterator(table_index_.GetIndex(0)->GetName()))); + return iter; + } + // clsutered and covering still use old iterator + return segment->NewIterator(spk, ts_col->GetId(), ticket, GetCompressType()); + } + // cidx without ts? or invalid case + DLOG(INFO) << "index ts col is null, reate no-ts iterator"; + // TODO(hw): sidx without ts? + return segment->NewIterator(spk, ticket, GetCompressType()); +} + +TraverseIterator* IndexOrganizedTable::NewTraverseIterator(uint32_t index) { + std::shared_ptr index_def = GetIndex(index); + if (!index_def || !index_def->IsReady()) { + PDLOG(WARNING, "index %u not found. tid %u pid %u", index, id_, pid_); + return nullptr; + } + DLOG(INFO) << "new traverse iter for index " << index << " name " << index_def->GetName(); + uint64_t expire_time = 0; + uint64_t expire_cnt = 0; + auto ttl = index_def->GetTTL(); + if (GetExpireStatus()) { // gc enabled + expire_time = GetExpireTime(*ttl); + expire_cnt = ttl->lat_ttl; + } + uint32_t real_idx = index_def->GetInnerPos(); + auto ts_col = index_def->GetTsColumn(); + if (ts_col) { + // if secondary, use iot iterator + if (index_def->IsSecondaryIndex()) { + // get clustered index iter for secondary index + auto handler = catalog_->GetTable(GetDB(), GetName()); + if (!handler) { + LOG(WARNING) << "no TableHandler for " << GetDB() << "." << GetName(); + return nullptr; + } + auto tablet_table_handler = std::dynamic_pointer_cast(handler); + if (!tablet_table_handler) { + LOG(WARNING) << "convert TabletTableHandler failed for " << GetDB() << "." << GetName(); + return nullptr; + } + LOG(INFO) << "create iot traverse iterator for traverse"; + // TODO(hw): iter may be invalid if catalog updated + auto iter = new IOTTraverseIterator( + GetSegments(real_idx), GetSegCnt(), ttl->ttl_type, expire_time, expire_cnt, ts_col->GetId(), + GetCompressType(), + std::move(tablet_table_handler->GetWindowIterator(table_index_.GetIndex(0)->GetName()))); + return iter; + } + DLOG(INFO) << "create memtable traverse iterator for traverse"; + return new MemTableTraverseIterator(GetSegments(real_idx), GetSegCnt(), ttl->ttl_type, expire_time, expire_cnt, + ts_col->GetId(), GetCompressType()); + } + DLOG(INFO) << "index ts col is null, reate no-ts iterator"; + return new MemTableTraverseIterator(GetSegments(real_idx), GetSegCnt(), ttl->ttl_type, expire_time, expire_cnt, 0, + GetCompressType()); +} + +::hybridse::vm::WindowIterator* IndexOrganizedTable::NewWindowIterator(uint32_t index) { + std::shared_ptr index_def = table_index_.GetIndex(index); + if (!index_def || !index_def->IsReady()) { + LOG(WARNING) << "index id " << index << " not found. tid " << id_ << " pid " << pid_; + return nullptr; + } + LOG(INFO) << "new window iter for index " << index << " name " << index_def->GetName(); + uint64_t expire_time = 0; + uint64_t expire_cnt = 0; + auto ttl = index_def->GetTTL(); + if (GetExpireStatus()) { + expire_time = GetExpireTime(*ttl); + expire_cnt = ttl->lat_ttl; + } + uint32_t real_idx = index_def->GetInnerPos(); + auto ts_col = index_def->GetTsColumn(); + uint32_t ts_idx = 0; + if (ts_col) { + ts_idx = ts_col->GetId(); + } + DLOG(INFO) << "ts is null? " << ts_col << ", ts_idx " << ts_idx; + // if secondary, use iot iterator + if (index_def->IsSecondaryIndex()) { + // get clustered index iter for secondary index + auto handler = catalog_->GetTable(GetDB(), GetName()); + if (!handler) { + LOG(WARNING) << "no TableHandler for " << GetDB() << "." << GetName(); + return nullptr; + } + auto tablet_table_handler = std::dynamic_pointer_cast(handler); + if (!tablet_table_handler) { + LOG(WARNING) << "convert TabletTableHandler failed for " << GetDB() << "." << GetName(); + return nullptr; + } + LOG(INFO) << "create iot key traverse iterator for window"; + // TODO(hw): iter may be invalid if catalog updated + auto iter = + new IOTKeyIterator(GetSegments(real_idx), GetSegCnt(), ttl->ttl_type, expire_time, expire_cnt, ts_idx, + GetCompressType(), tablet_table_handler, table_index_.GetIndex(0)->GetName()); + return iter; + } + return new MemTableKeyIterator(GetSegments(real_idx), GetSegCnt(), ttl->ttl_type, expire_time, expire_cnt, ts_idx, + GetCompressType()); +} + +bool IndexOrganizedTable::Init() { + if (!InitMeta()) { + LOG(WARNING) << "init meta failed. tid " << id_ << " pid " << pid_; + return false; + } + // IOTSegment should know which is the cidx, sidx and covering idx are both duplicate(even the values are different) + auto inner_indexs = table_index_.GetAllInnerIndex(); + for (uint32_t i = 0; i < inner_indexs->size(); i++) { + const std::vector& ts_vec = inner_indexs->at(i)->GetTsIdx(); + uint32_t cur_key_entry_max_height = KeyEntryMaxHeight(inner_indexs->at(i)); + + Segment** seg_arr = new Segment*[seg_cnt_]; + DLOG_ASSERT(!ts_vec.empty()) << "must have ts, include auto gen ts"; + if (!ts_vec.empty()) { + for (uint32_t j = 0; j < seg_cnt_; j++) { + // let segment know whether it is cidx + seg_arr[j] = new IOTSegment(cur_key_entry_max_height, ts_vec, inner_indexs->at(i)->GetTsIdxType()); + PDLOG(INFO, "init %u, %u segment. height %u, ts col num %u. tid %u pid %u", i, j, + cur_key_entry_max_height, ts_vec.size(), id_, pid_); + } + } else { + // unavaildable + for (uint32_t j = 0; j < seg_cnt_; j++) { + seg_arr[j] = new IOTSegment(cur_key_entry_max_height); + PDLOG(INFO, "init %u, %u segment. height %u tid %u pid %u", i, j, cur_key_entry_max_height, id_, pid_); + } + } + segments_[i] = seg_arr; + key_entry_max_height_ = cur_key_entry_max_height; + } + LOG(INFO) << "init iot table name " << name_ << ", id " << id_ << ", pid " << pid_ << ", seg_cnt " << seg_cnt_; + return true; +} + +bool IndexOrganizedTable::Put(const std::string& pk, uint64_t time, const char* data, uint32_t size) { + uint32_t seg_idx = SegIdx(pk); + Segment* segment = GetSegment(0, seg_idx); + if (segment == nullptr) { + return false; + } + Slice spk(pk); + segment->Put(spk, time, data, size); + record_byte_size_.fetch_add(GetRecordSize(size)); + return true; +} + +absl::Status IndexOrganizedTable::Put(uint64_t time, const std::string& value, const Dimensions& dimensions, + bool put_if_absent) { + if (dimensions.empty()) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": empty dimension")); + } + // inner index pos: -1 means invalid, so it's positive in inner_index_key_map + std::map inner_index_key_map; + std::pair cidx_inner_key_pair{-1, ""}; + std::vector secondary_inners; + for (auto iter = dimensions.begin(); iter != dimensions.end(); iter++) { + int32_t inner_pos = table_index_.GetInnerIndexPos(iter->idx()); + if (inner_pos < 0) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": invalid dimension idx ", iter->idx())); + } + if (iter->idx() == 0) { + cidx_inner_key_pair = {inner_pos, iter->key()}; + } + inner_index_key_map.emplace(inner_pos, iter->key()); + } + + const int8_t* data = reinterpret_cast(value.data()); + std::string uncompress_data; + uint32_t data_length = value.length(); + if (GetCompressType() == openmldb::type::kSnappy) { + snappy::Uncompress(value.data(), value.size(), &uncompress_data); + data = reinterpret_cast(uncompress_data.data()); + data_length = uncompress_data.length(); + } + if (data_length < codec::HEADER_LENGTH) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": invalid value")); + } + uint8_t version = codec::RowView::GetSchemaVersion(data); + auto decoder = GetVersionDecoder(version); + if (decoder == nullptr) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": invalid schema version ", version)); + } + std::optional clustered_tsv; + std::map> ts_value_map; + // we need two ref cnt + // 1. clustered and covering: put row -> DataBlock(i) + // 2. secondary: put pkeys+pts -> DataBlock(j) + uint32_t real_ref_cnt = 0, secondary_ref_cnt = 0; + // cidx_inner_key_pair can get the clustered index + for (const auto& kv : inner_index_key_map) { + auto inner_index = table_index_.GetInnerIndex(kv.first); + if (!inner_index) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": invalid inner index pos ", kv.first)); + } + std::map ts_map; + for (const auto& index_def : inner_index->GetIndex()) { + if (!index_def->IsReady()) { + continue; + } + auto ts_col = index_def->GetTsColumn(); + if (ts_col) { + int64_t ts = 0; + if (ts_col->IsAutoGenTs()) { + // clustered index still use current time to ttl and delete iter, we'll check time series size if ts + // is auto gen + ts = time; + } else if (decoder->GetInteger(data, ts_col->GetId(), ts_col->GetType(), &ts) != 0) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": get ts failed")); + } + if (ts < 0) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": ts is negative ", ts)); + } + // TODO(hw): why uint32_t to int32_t? + ts_map.emplace(ts_col->GetId(), ts); + + if (index_def->IsSecondaryIndex()) { + secondary_ref_cnt++; + } else { + real_ref_cnt++; + } + if (index_def->IsClusteredIndex()) { + clustered_tsv = ts; + } + } + } + if (!ts_map.empty()) { + ts_value_map.emplace(kv.first, std::move(ts_map)); + } + } + if (ts_value_map.empty()) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": empty ts value map")); + } + // it's ok to have no clustered/covering put or no secondary put, put will be applyed on other pid + // but if no clustered/covering put and no secondary put, it's invalid, check it in put-loop + DataBlock* cblock = nullptr; + DataBlock* sblock = nullptr; + if (real_ref_cnt > 0) { + cblock = new DataBlock(real_ref_cnt, value.c_str(), value.length()); // hard copy + } + if (secondary_ref_cnt > 0) { + // dimensions may not contain cidx, but we need cidx pkeys+pts for secondary index + // if contains, just use the key; if not, extract from value + if (cidx_inner_key_pair.first == -1) { + DLOG(INFO) << "cidx not in dimensions, extract from value"; + auto cidx = table_index_.GetIndex(0); + auto hint = base::MakePkeysHint(table_meta_->column_desc(), table_meta_->column_key(0)); + if (hint.empty()) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": cidx pkeys hint empty")); + } + cidx_inner_key_pair.second = + base::ExtractPkeys(table_meta_->column_key(0), (int8_t*)value.c_str(), *decoder, hint); + if (cidx_inner_key_pair.second.empty()) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": cidx pkeys+pts extract failed")); + } + DLOG_ASSERT(!clustered_tsv) << "clustered ts should not be set too"; + auto ts_col = cidx->GetTsColumn(); + if (!ts_col) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ":no ts column in cidx")); + } + int64_t ts = 0; + if (ts_col->IsAutoGenTs()) { + // clustered index still use current time to ttl and delete iter, we'll check time series size if ts is + // auto gen + ts = time; + } else if (decoder->GetInteger(data, ts_col->GetId(), ts_col->GetType(), &ts) != 0) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": get ts failed")); + } + if (ts < 0) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": ts is negative ", ts)); + } + clustered_tsv = ts; + } + auto pkeys_pts = PackPkeysAndPts(cidx_inner_key_pair.second, clustered_tsv.value()); + if (GetCompressType() == type::kSnappy) { // sidx iterator will uncompress when getting pkeys+pts + std::string val; + ::snappy::Compress(pkeys_pts.c_str(), pkeys_pts.length(), &val); + sblock = new DataBlock(secondary_ref_cnt, val.c_str(), val.length()); + } else { + sblock = new DataBlock(secondary_ref_cnt, pkeys_pts.c_str(), pkeys_pts.length()); // hard copy + } + } + DLOG(INFO) << "put iot table " << id_ << "." << pid_ << " key+ts " << cidx_inner_key_pair.second << " - " + << (clustered_tsv ? std::to_string(clustered_tsv.value()) : "-1") << ", real ref cnt " << real_ref_cnt + << " secondary ref cnt " << secondary_ref_cnt; + + for (const auto& kv : inner_index_key_map) { + auto iter = ts_value_map.find(kv.first); + if (iter == ts_value_map.end()) { + continue; + } + uint32_t seg_idx = SegIdx(kv.second.ToString()); + auto iot_segment = dynamic_cast(GetSegment(kv.first, seg_idx)); + // TODO(hw): put if absent unsupportted + if (put_if_absent) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": iot put if absent is not supported")); + } + // clustered segment should be dedup and update will trigger all index update(impl in cli router) + if (!iot_segment->Put(kv.second, iter->second, cblock, sblock, false)) { + // even no put_if_absent, return false if exists or wrong + return absl::AlreadyExistsError("data exists or wrong"); + } + } + // cblock and sblock both will sub record_byte_size_ when delete, so add them all + // TODO(hw): test for cal + if (real_ref_cnt > 0) { + record_byte_size_.fetch_add(GetRecordSize(cblock->size)); + } + if (secondary_ref_cnt > 0) { + record_byte_size_.fetch_add(GetRecordSize(sblock->size)); + } + + return absl::OkStatus(); +} + +absl::Status IndexOrganizedTable::CheckDataExists(uint64_t tsv, const Dimensions& dimensions) { + // get cidx dim + if (dimensions.empty()) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": empty dimension")); + } + // inner index pos: -1 means invalid, so it's positive in inner_index_key_map + std::pair cidx_inner_key_pair{-1, ""}; + for (auto iter = dimensions.begin(); iter != dimensions.end(); iter++) { + int32_t inner_pos = table_index_.GetInnerIndexPos(iter->idx()); + if (inner_pos < 0) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": invalid dimension idx ", iter->idx())); + } + if (iter->idx() == 0) { + cidx_inner_key_pair = {inner_pos, iter->key()}; + } + } + if (cidx_inner_key_pair.first == -1) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": cidx not found")); + } + auto cidx = table_index_.GetIndex(0); + if (!cidx->IsReady()) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": cidx is not ready")); + } + auto ts_col = cidx->GetTsColumn(); + if (!ts_col) { + return absl::InvalidArgumentError(absl::StrCat(id_, ".", pid_, ": no ts column")); + } + DLOG(INFO) << "check iot table " << id_ << "." << pid_ << " key+ts " << cidx_inner_key_pair.second << " - " << tsv + << ", on index " << cidx->GetName() << " ts col " << ts_col->GetId(); + + uint32_t seg_idx = SegIdx(cidx_inner_key_pair.second); + auto iot_segment = dynamic_cast(GetSegment(cidx_inner_key_pair.first, seg_idx)); + // ts id -> ts value + return iot_segment->CheckKeyExists(cidx_inner_key_pair.second, {{ts_col->GetId(), tsv}}); +} + +// index gc should try to do ExecuteGc for each waiting segment, but if some segments are gc before, we should release +// them so it will be a little complex +// should run under lock +absl::Status IndexOrganizedTable::ClusteredIndexGCByDelete(const std::shared_ptr& router) { + auto cur_index = table_index_.GetIndex(0); + if (!cur_index) { + return absl::FailedPreconditionError( + absl::StrCat("cidx def is null for ", id_, ".", pid_)); // why index is null? + } + if (!cur_index->IsClusteredIndex()) { + return absl::InternalError(absl::StrCat("cidx is not clustered for ", id_, ".", pid_)); // immpossible + } + if (!cur_index->IsReady()) { + return absl::FailedPreconditionError( + absl::StrCat("cidx is not ready for ", id_, ".", pid_, ", status ", cur_index->GetStatus())); + } + auto& ts_col = cur_index->GetTsColumn(); + // sometimes index def is valid, but ts_col is nullptr? protect it + if (!ts_col) { + return absl::FailedPreconditionError( + absl::StrCat("no ts col of cidx for ", id_, ".", pid_)); // current time ts can be get too + } + // clustered index grep all entries or less to delete(it's simpler to run delete sql) + // not the real gc, so don't change index status + auto i = cur_index->GetId(); + std::map ttl_st_map; + // only set cidx + ttl_st_map.emplace(ts_col->GetId(), *cur_index->GetTTL()); + GCEntryInfo info; // not thread safe + for (uint32_t j = 0; j < seg_cnt_; j++) { + uint64_t seg_gc_time = ::baidu::common::timer::get_micros() / 1000; + Segment* segment = segments_[i][j]; + auto iot_segment = dynamic_cast(segment); + iot_segment->GrepGCEntry(ttl_st_map, &info); + seg_gc_time = ::baidu::common::timer::get_micros() / 1000 - seg_gc_time; + PDLOG(INFO, "grep cidx segment[%u][%u] gc entries done consumed %lu for table %s tid %u pid %u", i, j, + seg_gc_time, name_.c_str(), id_, pid_); + } + // delete entries by sql + if (info.Size() > 0) { + LOG(INFO) << "delete cidx " << info.Size() << " entries by sql"; + auto meta = GetTableMeta(); + auto cols = meta->column_desc(); // copy + codec::RowView row_view(cols); + auto hint = base::MakePkeysHint(cols, meta->column_key(0)); + if (hint.empty()) { + return absl::InternalError("make pkeys hint failed"); + } + for (size_t i = 0; i < info.Size(); i++) { + auto& keys_ts = info.GetEntries()[i]; + auto values = keys_ts.second; // get pkeys from values + auto ts = keys_ts.first; + auto sql = + base::MakeDeleteSQL(GetDB(), GetName(), meta->column_key(0), (int8_t*)values->data, ts, row_view, hint); + // TODO(hw): if delete failed, we can't revert. And if sidx skeys+sts doesn't change, no need to delete and + // then insert + if (sql.empty()) { + return absl::InternalError("make delete sql failed"); + } + // delete will move node to node cache, it's alive, so GCEntryInfo can unref it + hybridse::sdk::Status status; + router->ExecuteSQL(sql, &status); + if (!status.IsOK()) { + return absl::InternalError("execute sql failed " + status.ToString()); + } + } + } + + return absl::OkStatus(); +} + +// TODO(hw): don't refactor with MemTable, make MemTable stable +void IndexOrganizedTable::SchedGCByDelete(const std::shared_ptr& router) { + std::lock_guard lock(gc_lock_); + uint64_t consumed = ::baidu::common::timer::get_micros(); + if (!enable_gc_.load(std::memory_order_relaxed)) { + LOG(INFO) << "iot table " << name_ << "[" << id_ << "." << pid_ << "] gc disabled"; + return; + } + LOG(INFO) << "iot table " << name_ << "[" << id_ << "." << pid_ << "] start making gc"; + // gc cidx first, it'll delete on all indexes + auto st = ClusteredIndexGCByDelete(router); + if (!st.ok()) { + LOG(WARNING) << "cidx gc by delete error: " << st.ToString(); + } + // TODO how to check the record byte size? + uint64_t gc_idx_cnt = 0; + uint64_t gc_record_byte_size = 0; + auto inner_indexs = table_index_.GetAllInnerIndex(); + for (uint32_t i = 0; i < inner_indexs->size(); i++) { + const std::vector>& real_index = inner_indexs->at(i)->GetIndex(); + std::map ttl_st_map; + bool need_gc = true; + size_t deleted_num = 0; + std::vector deleting_pos; + for (size_t pos = 0; pos < real_index.size(); pos++) { + auto cur_index = real_index[pos]; + auto ts_col = cur_index->GetTsColumn(); + if (ts_col) { + ttl_st_map.emplace(ts_col->GetId(), *(cur_index->GetTTL())); + } + if (cur_index->GetStatus() == IndexStatus::kWaiting) { + cur_index->SetStatus(IndexStatus::kDeleting); + need_gc = false; + } else if (cur_index->GetStatus() == IndexStatus::kDeleting) { + deleting_pos.push_back(pos); + } else if (cur_index->GetStatus() == IndexStatus::kDeleted) { + deleted_num++; + } + } + if (!deleting_pos.empty()) { + if (segments_[i] != nullptr) { + for (uint32_t k = 0; k < seg_cnt_; k++) { + if (segments_[i][k] != nullptr) { + StatisticsInfo statistics_info(segments_[i][k]->GetTsCnt()); + if (real_index.size() == 1 || deleting_pos.size() + deleted_num == real_index.size()) { + segments_[i][k]->ReleaseAndCount(&statistics_info); + } else { + segments_[i][k]->ReleaseAndCount(deleting_pos, &statistics_info); + } + gc_idx_cnt += statistics_info.GetTotalCnt(); + gc_record_byte_size += statistics_info.record_byte_size; + LOG(INFO) << "release segment[" << i << "][" << k << "] done, gc record cnt " + << statistics_info.GetTotalCnt() << ", gc record byte size " + << statistics_info.record_byte_size; + } + } + } + for (auto pos : deleting_pos) { + real_index[pos]->SetStatus(IndexStatus::kDeleted); + } + deleted_num += deleting_pos.size(); + } + if (!need_gc) { + continue; + } + // skip cidx gc in segment, gcfreelist shouldn't be skiped, so we don't change the condition + if (deleted_num == real_index.size() || ttl_st_map.empty()) { + continue; + } + for (uint32_t j = 0; j < seg_cnt_; j++) { + uint64_t seg_gc_time = ::baidu::common::timer::get_micros() / 1000; + auto segment = dynamic_cast(segments_[i][j]); + StatisticsInfo statistics_info(segment->GetTsCnt()); + segment->IncrGcVersion(); + segment->GcFreeList(&statistics_info); + // don't gc in cidx, it's not a good way to impl, refactor later + segment->ExecuteGc(ttl_st_map, &statistics_info, segment->ClusteredTs()); + gc_idx_cnt += statistics_info.GetTotalCnt(); + gc_record_byte_size += statistics_info.record_byte_size; + seg_gc_time = ::baidu::common::timer::get_micros() / 1000 - seg_gc_time; + VLOG(1) << "gc segment[" << i << "][" << j << "] done, consumed time " << seg_gc_time << "ms for table " + << name_ << "[" << id_ << "." << pid_ << "], statistics_info: [" << statistics_info.DebugString() + << "]"; + } + } + consumed = ::baidu::common::timer::get_micros() - consumed; + LOG(INFO) << "record byte size before gc: " << record_byte_size_.load() + << ", gc record byte size: " << gc_record_byte_size << ", gc idx cnt: " << gc_idx_cnt + << ", gc consumed: " << consumed / 1000 << " ms"; + record_byte_size_.fetch_sub(gc_record_byte_size, std::memory_order_relaxed); + UpdateTTL(); + LOG(INFO) << "update ttl done"; +} + +bool IndexOrganizedTable::AddIndexToTable(const std::shared_ptr& index_def) { + std::vector ts_vec = {index_def->GetTsColumn()->GetId()}; + uint32_t inner_id = index_def->GetInnerPos(); + Segment** seg_arr = new Segment*[seg_cnt_]; + for (uint32_t j = 0; j < seg_cnt_; j++) { + seg_arr[j] = new IOTSegment(FLAGS_absolute_default_skiplist_height, ts_vec, {index_def->GetIndexType()}); + LOG(INFO) << "init iot segment inner_ts" << inner_id << "." << j << " for table " << name_ << "[" << id_ << "." + << pid_ << "], height " << FLAGS_absolute_default_skiplist_height << ", ts col num " << ts_vec.size() + << ", type " << IndexType_Name(index_def->GetIndexType()); + } + segments_[inner_id] = seg_arr; + return true; +} + +} // namespace openmldb::storage diff --git a/src/storage/index_organized_table.h b/src/storage/index_organized_table.h new file mode 100644 index 00000000000..014e1a56a0a --- /dev/null +++ b/src/storage/index_organized_table.h @@ -0,0 +1,68 @@ +/* + * Copyright 2021 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SRC_STORAGE_INDEX_ORGANIZED_TABLE_H_ +#define SRC_STORAGE_INDEX_ORGANIZED_TABLE_H_ + +#include + +#include "catalog/tablet_catalog.h" +#include "storage/mem_table.h" + +namespace openmldb::storage { + +class IndexOrganizedTable : public MemTable { + public: + IndexOrganizedTable(const ::openmldb::api::TableMeta& table_meta, std::shared_ptr catalog) + : MemTable(table_meta), catalog_(catalog) {} + + TableIterator* NewIterator(uint32_t index, const std::string& pk, Ticket& ticket) override; + + TraverseIterator* NewTraverseIterator(uint32_t index) override; + + ::hybridse::vm::WindowIterator* NewWindowIterator(uint32_t index) override; + + bool Init() override; + + bool Put(const std::string& pk, uint64_t time, const char* data, uint32_t size) override; + + absl::Status Put(uint64_t time, const std::string& value, const Dimensions& dimensions, + bool put_if_absent) override; + + absl::Status CheckDataExists(uint64_t tsv, const Dimensions& dimensions); + + // TODO(hw): iot bulk load unsupported + bool GetBulkLoadInfo(::openmldb::api::BulkLoadInfoResponse* response) { return false; } + bool BulkLoad(const std::vector& data_blocks, + const ::google::protobuf::RepeatedPtrField<::openmldb::api::BulkLoadIndex>& indexes) { + return false; + } + bool AddIndexToTable(const std::shared_ptr& index_def) override; + + void SchedGCByDelete(const std::shared_ptr& router); + + private: + absl::Status ClusteredIndexGCByDelete(const std::shared_ptr& router); + + private: + // to get current distribute iterator + std::shared_ptr catalog_; + + std::mutex gc_lock_; +}; + +} // namespace openmldb::storage +#endif diff --git a/src/storage/iot_segment.cc b/src/storage/iot_segment.cc new file mode 100644 index 00000000000..89a19e4838f --- /dev/null +++ b/src/storage/iot_segment.cc @@ -0,0 +1,412 @@ +/* + * Copyright 2021 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "storage/iot_segment.h" + +#include "iot_segment.h" + +namespace openmldb::storage { +base::Slice RowToSlice(const ::hybridse::codec::Row& row) { + butil::IOBuf buf; + size_t size; + if (codec::EncodeRpcRow(row, &buf, &size)) { + auto r = new char[buf.size()]; + buf.copy_to(r); // TODO(hw): don't copy, move it to slice + // slice own the new r + return {r, size, true}; + } + LOG(WARNING) << "convert row to slice failed"; + return {}; +} + +std::string PackPkeysAndPts(const std::string& pkeys, uint64_t pts) { + std::string buf; + uint32_t pkeys_size = pkeys.size(); + buf.append(reinterpret_cast(&pkeys_size), sizeof(uint32_t)); + buf.append(pkeys); + buf.append(reinterpret_cast(&pts), sizeof(uint64_t)); + return buf; +} + +bool UnpackPkeysAndPts(const std::string& block, std::string* pkeys, uint64_t* pts) { + DLOG_ASSERT(block.size() >= sizeof(uint32_t) + sizeof(uint64_t)) << "block size is " << block.size(); + uint32_t offset = 0; + uint32_t pkeys_size = *reinterpret_cast(block.data() + offset); + offset += sizeof(uint32_t); + pkeys->assign(block.data() + offset, pkeys_size); + offset += pkeys_size; + *pts = *reinterpret_cast(block.data() + offset); + DLOG_ASSERT(offset + sizeof(uint64_t) == block.size()) + << "offset is " << offset << " block size is " << block.size(); + return true; +} + +// put_if_absent unsupported, iot table will reject put, no need to check here, just ignore +bool IOTSegment::PutUnlock(const Slice& key, uint64_t time, DataBlock* row, bool put_if_absent, bool auto_gen_ts) { + void* entry = nullptr; + uint32_t byte_size = 0; + // one key just one entry + int ret = entries_->Get(key, entry); + if (ret < 0 || entry == nullptr) { + char* pk = new char[key.size()]; + memcpy(pk, key.data(), key.size()); + // need to delete memory when free node + Slice skey(pk, key.size()); + entry = reinterpret_cast(new KeyEntry(key_entry_max_height_)); + uint8_t height = entries_->Insert(skey, entry); + byte_size += GetRecordPkIdxSize(height, key.size(), key_entry_max_height_); + pk_cnt_.fetch_add(1, std::memory_order_relaxed); + // no need to check if absent when first put + } else if (IsClusteredTs(ts_idx_map_.begin()->first)) { + // if cidx and key match, check ts -> insert or update + if (auto_gen_ts) { + // cidx(keys) has just one entry for one keys, so if keys exists, needs delete + LOG_IF(ERROR, reinterpret_cast(entry)->entries.GetSize() > 1) + << "cidx keys has more than one entry, " << reinterpret_cast(entry)->entries.GetSize(); + // TODO(hw): client will delete old row, so if pkeys exists when auto ts, fail it + return false; + } else { + // cidx(keys+ts) check if ts match + if (ListContains(reinterpret_cast(entry), time, row, false)) { + LOG(WARNING) << "key " << key.ToString() << " ts " << time << " exists in cidx"; + return false; + } + } + } + + idx_cnt_vec_[0]->fetch_add(1, std::memory_order_relaxed); + uint8_t height = reinterpret_cast(entry)->entries.Insert(time, row); + reinterpret_cast(entry)->count_.fetch_add(1, std::memory_order_relaxed); + byte_size += GetRecordTsIdxSize(height); + idx_byte_size_.fetch_add(byte_size, std::memory_order_relaxed); + DLOG(INFO) << "idx_byte_size_ " << idx_byte_size_ << " after add " << byte_size; + return true; +} + +bool IOTSegment::Put(const Slice& key, const std::map& ts_map, DataBlock* cblock, DataBlock* sblock, + bool put_if_absent) { + if (ts_map.empty()) { + return false; + } + if (ts_cnt_ == 1) { + bool ret = false; + if (auto pos = ts_map.find(ts_idx_map_.begin()->first); pos != ts_map.end()) { + // TODO(hw): why ts_map key is int32_t, default ts is uint32_t? + ret = Segment::Put(key, pos->second, + (index_types_[ts_idx_map_.begin()->second] == common::kSecondary ? sblock : cblock), + false, pos->first == DEFAULT_TS_COL_ID); + } + return ret; + } + void* entry_arr = nullptr; + std::lock_guard lock(mu_); + for (const auto& kv : ts_map) { + uint32_t byte_size = 0; + auto pos = ts_idx_map_.find(kv.first); + if (pos == ts_idx_map_.end()) { + continue; + } + if (entry_arr == nullptr) { + int ret = entries_->Get(key, entry_arr); + if (ret < 0 || entry_arr == nullptr) { + char* pk = new char[key.size()]; + memcpy(pk, key.data(), key.size()); + Slice skey(pk, key.size()); + KeyEntry** entry_arr_tmp = new KeyEntry*[ts_cnt_]; + for (uint32_t i = 0; i < ts_cnt_; i++) { + entry_arr_tmp[i] = new KeyEntry(key_entry_max_height_); + } + entry_arr = reinterpret_cast(entry_arr_tmp); + uint8_t height = entries_->Insert(skey, entry_arr); + byte_size += GetRecordPkMultiIdxSize(height, key.size(), key_entry_max_height_, ts_cnt_); + pk_cnt_.fetch_add(1, std::memory_order_relaxed); + } + } + auto entry = reinterpret_cast(entry_arr)[pos->second]; + auto auto_gen_ts = (pos->first == DEFAULT_TS_COL_ID); + auto pblock = (index_types_[pos->second] == common::kSecondary ? sblock : cblock); + if (IsClusteredTs(pos->first)) { + // if cidx and key match, check ts -> insert or update + if (auto_gen_ts) { + // cidx(keys) has just one entry for one keys, so if keys exists, needs delete + LOG_IF(ERROR, reinterpret_cast(entry)->entries.GetSize() > 1) + << "cidx keys has more than one entry, " << reinterpret_cast(entry)->entries.GetSize(); + // TODO(hw): client will delete old row, so if pkeys exists when auto ts, fail it + if (reinterpret_cast(entry)->entries.GetSize() > 0) { + LOG(WARNING) << "key " << key.ToString() << " exists in cidx"; + return false; + } + } else { + // cidx(keys+ts) check if ts match + if (ListContains(reinterpret_cast(entry), kv.second, pblock, false)) { + LOG(WARNING) << "key " << key.ToString() << " ts " << kv.second << " exists in cidx"; + return false; + } + } + } + uint8_t height = entry->entries.Insert(kv.second, pblock); + entry->count_.fetch_add(1, std::memory_order_relaxed); + byte_size += GetRecordTsIdxSize(height); + idx_byte_size_.fetch_add(byte_size, std::memory_order_relaxed); + DLOG(INFO) << "idx_byte_size_ " << idx_byte_size_ << " after add " << byte_size; + idx_cnt_vec_[pos->second]->fetch_add(1, std::memory_order_relaxed); + } + return true; +} + +absl::Status IOTSegment::CheckKeyExists(const Slice& key, const std::map& ts_map) { + // check lock + void* entry_arr = nullptr; + std::lock_guard lock(mu_); // need shrink? + int ret = entries_->Get(key, entry_arr); + if (ret < 0 || entry_arr == nullptr) { + return absl::NotFoundError("key not found"); + } + if (ts_map.size() != 1) { + return absl::InvalidArgumentError("ts map size is not 1"); + } + auto idx_ts = ts_map.begin(); + auto pos = ts_idx_map_.find(idx_ts->first); + if (pos == ts_idx_map_.end()) { + return absl::InvalidArgumentError("ts not found"); + } + // be careful, ts id in arg maybe negative cuz it's int32, but id in member is uint32 + if (!IsClusteredTs(idx_ts->first)) { + LOG(WARNING) << "idx_ts->first " << idx_ts->first << " is not clustered ts " + << (clustered_ts_id_.has_value() ? std::to_string(clustered_ts_id_.value()) : "no"); + return absl::InvalidArgumentError("ts is not clustered"); + } + KeyEntry* entry = nullptr; + if (ts_cnt_ == 1) { + LOG_IF(ERROR, pos->second != 0) << "when ts cnt == 1, pos second is " << pos->second; + entry = reinterpret_cast(entry_arr); + } else { + entry = reinterpret_cast(entry_arr)[pos->second]; + } + + if (entry == nullptr) { + return absl::NotFoundError("ts entry not found"); + } + auto auto_gen_ts = (idx_ts->first == DEFAULT_TS_COL_ID); + if (auto_gen_ts) { + // cidx(keys) has just one entry for one keys, so if keys exists, needs delete + DLOG_ASSERT(reinterpret_cast(entry)->entries.GetSize() == 1) << "cidx keys has more than one entry"; + if (reinterpret_cast(entry)->entries.GetSize() > 0) { + return absl::AlreadyExistsError("key exists: " + key.ToString()); + } + } else { + // don't use listcontains, we don't need to check value, just check if time exists + storage::DataBlock* v = nullptr; + if (entry->entries.Get(idx_ts->second, v) == 0) { + return absl::AlreadyExistsError(absl::StrCat("key+ts exists: ", key.ToString(), ", ts ", idx_ts->second)); + } + } + + return absl::NotFoundError("ts not found"); +} +// TODO(hw): when add lock? ref segment, don't lock iter +void IOTSegment::GrepGCEntry(const std::map& ttl_st_map, GCEntryInfo* gc_entry_info) { + if (ttl_st_map.empty()) { + DLOG(INFO) << "ttl map is empty, skip gc"; + return; + } + + bool need_gc = false; + for (const auto& kv : ttl_st_map) { + if (ts_idx_map_.find(kv.first) == ts_idx_map_.end()) { + LOG(WARNING) << "ts idx " << kv.first << " not found"; + return; + } + if (kv.second.NeedGc()) { + need_gc = true; + } + } + if (!need_gc) { + DLOG(INFO) << "no need gc, skip gc"; + return; + } + GrepGCAllType(ttl_st_map, gc_entry_info); +} + +void GrepGC4Abs(KeyEntry* entry, const Slice& key, const TTLSt& ttl, uint64_t cur_time, uint64_t ttl_offset, + GCEntryInfo* gc_entry_info) { + if (ttl.abs_ttl == 0) { + return; // never expire + } + uint64_t expire_time = cur_time - ttl_offset - ttl.abs_ttl; + std::unique_ptr iter(entry->entries.NewIterator()); + iter->Seek(expire_time); + // delete (expire, last] + while (iter->Valid()) { + if (iter->GetKey() > expire_time) { + break; + } + // expire_time has offset, so we don't need to check if equal + // if (iter->GetKey() == expire_time) { + // continue; // save ==, don't gc + // } + gc_entry_info->AddEntry(key, iter->GetKey(), iter->GetValue()); + if (gc_entry_info->Full()) { + LOG(INFO) << "gc entry info full, stop gc grep"; + return; + } + iter->Next(); + } +} + +void GrepGC4Lat(KeyEntry* entry, const Slice& key, const TTLSt& ttl, GCEntryInfo* gc_entry_info) { + auto keep_cnt = ttl.lat_ttl; + if (keep_cnt == 0) { + return; // never exipre + } + + std::unique_ptr iter(entry->entries.NewIterator()); + iter->SeekToFirst(); + while (iter->Valid()) { + if (keep_cnt > 0) { + keep_cnt--; + } else { + gc_entry_info->AddEntry(key, iter->GetKey(), iter->GetValue()); + } + if (gc_entry_info->Full()) { + LOG(INFO) << "gc entry info full, stop gc grep"; + return; + } + iter->Next(); + } +} + +void GrepGC4AbsAndLat(KeyEntry* entry, const Slice& key, const TTLSt& ttl, uint64_t cur_time, uint64_t ttl_offset, + GCEntryInfo* gc_entry_info) { + if (ttl.abs_ttl == 0 || ttl.lat_ttl == 0) { + return; // never exipre + } + // keep both + uint64_t expire_time = cur_time - ttl_offset - ttl.abs_ttl; + auto keep_cnt = ttl.lat_ttl; + std::unique_ptr iter(entry->entries.NewIterator()); + iter->SeekToFirst(); + // if > lat cnt and < expire, delete + while (iter->Valid()) { + if (keep_cnt > 0) { + keep_cnt--; + } else if (iter->GetKey() < expire_time) { + gc_entry_info->AddEntry(key, iter->GetKey(), iter->GetValue()); + } + if (gc_entry_info->Full()) { + LOG(INFO) << "gc entry info full, stop gc grep"; + return; + } + iter->Next(); + } +} +void GrepGC4AbsOrLat(KeyEntry* entry, const Slice& key, const TTLSt& ttl, uint64_t cur_time, uint64_t ttl_offset, + GCEntryInfo* gc_entry_info) { + if (ttl.abs_ttl == 0 && ttl.lat_ttl == 0) { + return; + } + if (ttl.abs_ttl == 0) { + // == lat ttl + GrepGC4Lat(entry, key, ttl, gc_entry_info); + return; + } + if (ttl.lat_ttl == 0) { + GrepGC4Abs(entry, key, ttl, cur_time, ttl_offset, gc_entry_info); + return; + } + uint64_t expire_time = cur_time - ttl_offset - ttl.abs_ttl; + auto keep_cnt = ttl.lat_ttl; + std::unique_ptr iter(entry->entries.NewIterator()); + iter->SeekToFirst(); + // if > keep cnt or < expire time, delete + while (iter->Valid()) { + if (keep_cnt > 0) { + keep_cnt--; // safe + } else { + gc_entry_info->AddEntry(key, iter->GetKey(), iter->GetValue()); + iter->Next(); + continue; + } + if (iter->GetKey() < expire_time) { + gc_entry_info->AddEntry(key, iter->GetKey(), iter->GetValue()); + } + if (gc_entry_info->Full()) { + LOG(INFO) << "gc entry info full, stop gc grep"; + return; + } + iter->Next(); + } +} + +// actually only one ttl for cidx, clean up later +void IOTSegment::GrepGCAllType(const std::map& ttl_st_map, GCEntryInfo* gc_entry_info) { + uint64_t consumed = ::baidu::common::timer::get_micros(); + uint64_t cur_time = consumed / 1000; + std::unique_ptr it(entries_->NewIterator()); + it->SeekToFirst(); + while (it->Valid()) { + KeyEntry** entry_arr = reinterpret_cast(it->GetValue()); + Slice key = it->GetKey(); + it->Next(); + for (const auto& kv : ttl_st_map) { + DLOG(INFO) << "key " << key.ToString() << ", ts idx " << kv.first << ", ttl " << kv.second.ToString() + << ", ts_cnt_ " << ts_cnt_; + if (!kv.second.NeedGc()) { + continue; + } + auto pos = ts_idx_map_.find(kv.first); + if (pos == ts_idx_map_.end() || pos->second >= ts_cnt_) { + LOG(WARNING) << "gc ts idx " << kv.first << " not found"; + continue; + } + KeyEntry* entry = nullptr; + // time series :[(ts, row), ...], so get key means get ts + if (ts_cnt_ == 1) { + LOG_IF(DFATAL, pos->second != 0) << "when ts cnt == 1, pos second is " << pos->second; + entry = reinterpret_cast(entry_arr); + } else { + entry = entry_arr[pos->second]; + } + if (entry == nullptr) { + DLOG(DFATAL) << "entry is null, impossible"; + continue; + } + switch (kv.second.ttl_type) { + case ::openmldb::storage::TTLType::kAbsoluteTime: { + GrepGC4Abs(entry, key, kv.second, cur_time, ttl_offset_, gc_entry_info); + break; + } + case ::openmldb::storage::TTLType::kLatestTime: { + GrepGC4Lat(entry, key, kv.second, gc_entry_info); + break; + } + case ::openmldb::storage::TTLType::kAbsAndLat: { + GrepGC4AbsAndLat(entry, key, kv.second, cur_time, ttl_offset_, gc_entry_info); + break; + } + case ::openmldb::storage::TTLType::kAbsOrLat: { + GrepGC4AbsOrLat(entry, key, kv.second, cur_time, ttl_offset_, gc_entry_info); + break; + } + default: + return; + } + } + } + DLOG(INFO) << "[GC ts map] iot segment gc consumed " << (::baidu::common::timer::get_micros() - consumed) / 1000 + << "ms, gc entry size " << gc_entry_info->Size(); +} +} // namespace openmldb::storage diff --git a/src/storage/iot_segment.h b/src/storage/iot_segment.h new file mode 100644 index 00000000000..b610241f240 --- /dev/null +++ b/src/storage/iot_segment.h @@ -0,0 +1,298 @@ +/* + * Copyright 2021 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SRC_STORAGE_IOT_SEGMENT_H_ +#define SRC_STORAGE_IOT_SEGMENT_H_ + +#include "catalog/tablet_catalog.h" +#include "codec/row_codec.h" +#include "codec/row_iterator.h" +#include "codec/sql_rpc_row_codec.h" +#include "storage/mem_table_iterator.h" +#include "storage/segment.h" +#include "storage/table.h" // for storage::Schema + +DECLARE_uint32(cidx_gc_max_size); + +namespace openmldb::storage { + +base::Slice RowToSlice(const ::hybridse::codec::Row& row); + +// [pkeys_size, pkeys, pts_size, ts_id, tsv, ...] +std::string PackPkeysAndPts(const std::string& pkeys, uint64_t pts); +bool UnpackPkeysAndPts(const std::string& block, std::string* pkeys, uint64_t* pts); + +// secondary index iterator +// GetValue will lookup, and it may trigger rpc +class IOTIterator : public MemTableIterator { + public: + IOTIterator(TimeEntries::Iterator* it, type::CompressType compress_type, + std::unique_ptr<::hybridse::codec::WindowIterator> cidx_iter) + : MemTableIterator(it, compress_type), cidx_iter_(std::move(cidx_iter)) {} + virtual ~IOTIterator() {} + + openmldb::base::Slice GetValue() const override { + auto pkeys_pts = MemTableIterator::GetValue(); + std::string pkeys; + uint64_t ts; + if (!UnpackPkeysAndPts(pkeys_pts.ToString(), &pkeys, &ts)) { + LOG(WARNING) << "unpack pkeys and pts failed"; + return ""; + } + cidx_iter_->Seek(pkeys); + if (cidx_iter_->Valid()) { + // seek to ts + auto ts_iter = cidx_iter_->GetValue(); + ts_iter->Seek(ts); + if (ts_iter->Valid()) { + return RowToSlice(ts_iter->GetValue()); + } + } + // TODO(hw): Valid() to check row data? what if only one entry invalid? + return ""; + } + + private: + std::unique_ptr<::hybridse::codec::WindowIterator> cidx_iter_; +}; + +class IOTTraverseIterator : public MemTableTraverseIterator { + public: + IOTTraverseIterator(Segment** segments, uint32_t seg_cnt, ::openmldb::storage::TTLType ttl_type, + uint64_t expire_time, uint64_t expire_cnt, uint32_t ts_index, type::CompressType compress_type, + std::unique_ptr<::hybridse::codec::WindowIterator> cidx_iter) + : MemTableTraverseIterator(segments, seg_cnt, ttl_type, expire_time, expire_cnt, ts_index, compress_type), + cidx_iter_(std::move(cidx_iter)) {} + ~IOTTraverseIterator() override {} + + openmldb::base::Slice GetValue() const override { + auto pkeys_pts = MemTableTraverseIterator::GetValue(); + std::string pkeys; + uint64_t ts; + if (!UnpackPkeysAndPts(pkeys_pts.ToString(), &pkeys, &ts)) { + LOG(WARNING) << "unpack pkeys and pts failed"; + return ""; + } + // distribute cidx iter should seek to (key, ts) + DLOG(INFO) << "seek to " << pkeys << ", " << ts; + cidx_iter_->Seek(pkeys); + if (cidx_iter_->Valid()) { + // seek to ts + auto ts_iter_ = cidx_iter_->GetValue(); + ts_iter_->Seek(ts); + if (ts_iter_->Valid()) { + // TODO(hw): hard copy, or hold ts_iter to store value? IOTIterator should be the same. + DLOG(INFO) << "valid, " << ts_iter_->GetValue().ToString(); + return RowToSlice(ts_iter_->GetValue()); + } + } + LOG(WARNING) << "no suitable iter"; + return ""; // won't core, just no row for select? + } + + private: + std::unique_ptr<::hybridse::codec::WindowIterator> cidx_iter_; + std::unique_ptr ts_iter_; +}; + +class IOTWindowIterator : public MemTableWindowIterator { + public: + IOTWindowIterator(TimeEntries::Iterator* it, ::openmldb::storage::TTLType ttl_type, uint64_t expire_time, + uint64_t expire_cnt, type::CompressType compress_type, + std::unique_ptr<::hybridse::codec::WindowIterator> cidx_iter) + : MemTableWindowIterator(it, ttl_type, expire_time, expire_cnt, compress_type), + cidx_iter_(std::move(cidx_iter)) { + DLOG(INFO) << "create IOTWindowIterator"; + } + // for debug + void SetSchema(const codec::Schema& schema, const std::vector& pkeys_idx) { + pkeys_idx_ = pkeys_idx; + row_view_.reset(new codec::RowView(schema)); + } + const ::hybridse::codec::Row& GetValue() override { + auto pkeys_pts = MemTableWindowIterator::GetValue(); + if (pkeys_pts.empty()) { + LOG(WARNING) << "empty pkeys_pts for key " << GetKey(); + return dummy; + } + + // unpack the row and get pkeys+pts + // Row -> cols + std::string pkeys; + uint64_t ts; + if (!UnpackPkeysAndPts(pkeys_pts.ToString(), &pkeys, &ts)) { + LOG(WARNING) << "unpack pkeys and pts failed"; + return dummy; + } + // TODO(hw): what if no ts? it'll be 0 for temp + DLOG(INFO) << "pkeys=" << pkeys << ", ts=" << ts; + cidx_iter_->Seek(pkeys); + if (cidx_iter_->Valid()) { + // seek to ts + DLOG(INFO) << "seek to ts " << ts; + // hold the row iterator to avoid invalidation + cidx_ts_iter_ = std::move(cidx_iter_->GetValue()); + cidx_ts_iter_->Seek(ts); + // must be the same keys+ts + if (cidx_ts_iter_->Valid()) { + // DLOG(INFO) << "valid, is the same value? " << GetKeys(cidx_ts_iter_->GetValue()); + return cidx_ts_iter_->GetValue(); + } + } + // Valid() to check row data? what if only one entry invalid? + return dummy; + } + + private: + std::string GetKeys(const hybridse::codec::Row& pkeys_pts) { + std::string pkeys, key; // RowView Get will assign output, no need to clear + for (auto pkey_idx : pkeys_idx_) { + if (!pkeys.empty()) { + pkeys += "|"; + } + // TODO(hw): if null, append to key? + auto ret = row_view_->GetStrValue(pkeys_pts.buf(), pkey_idx, &key); + if (ret == -1) { + LOG(WARNING) << "get pkey failed"; + return {}; + } + pkeys += key.empty() ? hybridse::codec::EMPTY_STRING : key; + DLOG(INFO) << pkey_idx << "=" << key; + } + return pkeys; + } + + private: + std::unique_ptr<::hybridse::codec::WindowIterator> cidx_iter_; + std::unique_ptr cidx_ts_iter_; + // for debug + std::unique_ptr row_view_; + std::vector pkeys_idx_; + + ::hybridse::codec::Row dummy; +}; + +class IOTKeyIterator : public MemTableKeyIterator { + public: + IOTKeyIterator(Segment** segments, uint32_t seg_cnt, ::openmldb::storage::TTLType ttl_type, uint64_t expire_time, + uint64_t expire_cnt, uint32_t ts_index, type::CompressType compress_type, + std::shared_ptr cidx_handler, const std::string& cidx_name) + : MemTableKeyIterator(segments, seg_cnt, ttl_type, expire_time, expire_cnt, ts_index, compress_type) { + // cidx_iter will be used by RowIterator but it's unique, so create it when get RowIterator + cidx_handler_ = cidx_handler; + cidx_name_ = cidx_name; + } + + ~IOTKeyIterator() override {} + void SetSchema(const std::shared_ptr& schema, + const std::shared_ptr& cidx) { + schema_ = *schema; // copy + // pkeys idx + std::map col_idx_map; + for (int i = 0; i < schema_.size(); i++) { + col_idx_map[schema_[i].name()] = i; + } + pkeys_idx_.clear(); + for (auto pkey : cidx->GetColumns()) { + pkeys_idx_.emplace_back(col_idx_map[pkey.GetName()]); + } + } + ::hybridse::vm::RowIterator* GetRawValue() override { + DLOG(INFO) << "GetRawValue for key " << GetKey().ToString() << ", bind cidx " << cidx_name_; + TimeEntries::Iterator* it = GetTimeIter(); + auto cidx_iter = cidx_handler_->GetWindowIterator(cidx_name_); + auto iter = + new IOTWindowIterator(it, ttl_type_, expire_time_, expire_cnt_, compress_type_, std::move(cidx_iter)); + // iter->SetSchema(schema_, pkeys_idx_); + return iter; + } + + private: + std::shared_ptr cidx_handler_; + std::string cidx_name_; + // test + codec::Schema schema_; + std::vector pkeys_idx_; +}; + +class GCEntryInfo { + public: + typedef std::pair Entry; + ~GCEntryInfo() { + for (auto& entry : entries_) { + entry.second->dim_cnt_down--; + // data block should be moved to node_cache then delete + // I don't want delete block here + LOG_IF(ERROR, entry.second->dim_cnt_down == 0) << "dim_cnt_down=0 but no delete"; + } + } + void AddEntry(const Slice& keys, uint64_t ts, storage::DataBlock* ptr) { + // to avoid Block deleted before gc, add ref + ptr->dim_cnt_down++; // TODO(hw): no concurrency? or make sure under lock + entries_.emplace_back(ts, ptr); + } + std::size_t Size() { return entries_.size(); } + std::vector& GetEntries() { return entries_; } + bool Full() { return entries_.size() >= FLAGS_cidx_gc_max_size; } + + private: + // std::vector> entries_; + std::vector entries_; +}; + +class IOTSegment : public Segment { + public: + explicit IOTSegment(uint8_t height) : Segment(height) {} + IOTSegment(uint8_t height, const std::vector& ts_idx_vec, + const std::vector& index_types) + : Segment(height, ts_idx_vec), index_types_(index_types) { + // find clustered ts id + for (uint32_t i = 0; i < ts_idx_vec.size(); i++) { + if (index_types_[i] == common::kClustered) { + clustered_ts_id_ = ts_idx_vec[i]; + break; + } + } + } + ~IOTSegment() override {} + + bool PutUnlock(const Slice& key, uint64_t time, DataBlock* row, bool put_if_absent, bool check_all_time); + bool Put(const Slice& key, const std::map& ts_map, DataBlock* cblock, DataBlock* sblock, + bool put_if_absent = false); + // use ts map to get idx in entry_arr + // no ok status, exists or not found + absl::Status CheckKeyExists(const Slice& key, const std::map& ts_map); + // DEFAULT_TS_COL_ID is uint32_t max, so clsutered_ts_id_ can't have a init value, use std::optional + bool IsClusteredTs(uint32_t ts_id) { + return clustered_ts_id_.has_value() ? (ts_id == clustered_ts_id_.value()) : false; + } + + std::optional ClusteredTs() const { return clustered_ts_id_; } + + void GrepGCEntry(const std::map& ttl_st_map, GCEntryInfo* gc_entry_info); + + // if segment is not secondary idx, use normal NewIterator in Segment + + private: + void GrepGCAllType(const std::map& ttl_st_map, GCEntryInfo* gc_entry_info); + + private: + std::vector index_types_; + std::optional clustered_ts_id_; +}; + +} // namespace openmldb::storage +#endif // SRC_STORAGE_IOT_SEGMENT_H_ diff --git a/src/storage/iot_segment_test.cc b/src/storage/iot_segment_test.cc new file mode 100644 index 00000000000..312c92c5a87 --- /dev/null +++ b/src/storage/iot_segment_test.cc @@ -0,0 +1,517 @@ +/* + * Copyright 2021 4Paradigm + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "storage/iot_segment.h" + +#include +#include + +#include "absl/strings/str_cat.h" +#include "base/glog_wrapper.h" +#include "base/slice.h" +#include "gtest/gtest.h" +#include "storage/record.h" + +using ::openmldb::base::Slice; + +namespace openmldb { +namespace storage { + +// iotsegment is not the same with segment, so we need to test it separately +class IOTSegmentTest : public ::testing::Test { + public: + IOTSegmentTest() {} + ~IOTSegmentTest() {} +}; + +TEST_F(IOTSegmentTest, PutAndScan) { + IOTSegment segment(8, {1, 3, 5}, + {common::IndexType::kClustered, common::IndexType::kSecondary, common::IndexType::kCovering}); + Slice pk("test1"); + std::string value = "test0"; + auto cblk = new DataBlock(2, value.c_str(), value.size()); // 1 clustered + 1 covering, hard copy + auto sblk = new DataBlock(1, value.c_str(), value.size()); // 1 secondary, fake value, hard copy + // use the frenquently used Put method + ASSERT_TRUE(segment.Put(pk, {{1, 100}, {3, 300}, {5, 500}}, cblk, sblk)); + // if first one is clustered index, segment put will fail in the first time, no need to revert + ASSERT_FALSE(segment.Put(pk, {{1, 100}}, cblk, sblk)); + ASSERT_FALSE(segment.Put(pk, {{1, 100}, {3, 300}, {5, 500}}, cblk, sblk)); + ASSERT_EQ(1, (int64_t)segment.GetPkCnt()); + Ticket ticket; + // iter clustered(idx 1), not the secondary, don't create iot iter + std::unique_ptr it( + segment.Segment::NewIterator("test1", 1, ticket, type::CompressType::kNoCompress)); + it->Seek(500); // find less than + ASSERT_TRUE(it->Valid()); + ASSERT_EQ(100, (int64_t)it->GetKey()); + ::openmldb::base::Slice val = it->GetValue(); + std::string result(val.data(), val.size()); + ASSERT_EQ("test0", result); + it->Next(); + ASSERT_FALSE(it->Valid()); // just one row + + // if first one is not the clustered index, we can't know if it exists, be careful + ASSERT_TRUE(segment.Put(pk, {{3, 300}, {5, 500}}, nullptr, nullptr)); +} + +TEST_F(IOTSegmentTest, PutAndScanWhenDefaultTs) { + // in the same inner index, it won't have the same ts id + IOTSegment segment(8, {DEFAULT_TS_COL_ID, 3, 5}, + {common::IndexType::kClustered, common::IndexType::kSecondary, common::IndexType::kCovering}); + Slice pk("test1"); + std::string value = "test0"; + auto cblk = new DataBlock(2, value.c_str(), value.size()); // 1 clustered + 1 covering, hard copy + auto sblk = new DataBlock(1, value.c_str(), value.size()); // 1 secondary, fake value, hard copy + // use the frenquently used Put method + ASSERT_TRUE(segment.Put(pk, {{DEFAULT_TS_COL_ID, 100}, {3, 300}, {5, 500}}, cblk, sblk)); + // if first one is clustered index, segment put will fail in the first time, no need to revert + ASSERT_FALSE(segment.Put(pk, {{DEFAULT_TS_COL_ID, 100}}, cblk, sblk)); + ASSERT_FALSE(segment.Put(pk, {{DEFAULT_TS_COL_ID, 100}, {3, 300}, {5, 500}}, cblk, sblk)); + ASSERT_EQ(1, (int64_t)segment.GetPkCnt()); + Ticket ticket; + // iter clustered(idx 1), not the secondary, don't create iot iter + std::unique_ptr it( + segment.Segment::NewIterator("test1", DEFAULT_TS_COL_ID, ticket, type::CompressType::kNoCompress)); + it->Seek(500); // find less than + ASSERT_TRUE(it->Valid()); + ASSERT_EQ(100, (int64_t)it->GetKey()); + ::openmldb::base::Slice val = it->GetValue(); + std::string result(val.data(), val.size()); + ASSERT_EQ("test0", result); + it->Next(); + ASSERT_FALSE(it->Valid()); // just one row + + // if first one is not the clustered index, we can't know if it exists, be careful + ASSERT_TRUE(segment.Put(pk, {{3, 300}, {5, 500}}, nullptr, nullptr)); +} + +TEST_F(IOTSegmentTest, CheckKeyExists) { + IOTSegment segment(8, {1, 3, 5}, + {common::IndexType::kClustered, common::IndexType::kSecondary, common::IndexType::kCovering}); + Slice pk("test1"); + std::string value = "test0"; + auto cblk = new DataBlock(2, value.c_str(), value.size()); // 1 clustered + 1 covering, hard copy + auto sblk = new DataBlock(1, value.c_str(), value.size()); // 1 secondary, fake value, hard copy + // use the frenquently used Put method + segment.Put(pk, {{1, 100}, {3, 300}, {5, 500}}, cblk, sblk); + ASSERT_EQ(1, (int64_t)segment.GetPkCnt()); + // check if exists in cidx segment(including 'ttl expired but not gc') + auto st = segment.CheckKeyExists(pk, {{1, 100}}); + ASSERT_TRUE(absl::IsAlreadyExists(st)) << st.ToString(); + st = segment.CheckKeyExists(pk, {{1, 300}}); + ASSERT_TRUE(absl::IsNotFound(st)) << st.ToString(); + // check sidx/covering idx will fail + st = segment.CheckKeyExists(pk, {{3, 300}}); + ASSERT_TRUE(absl::IsInvalidArgument(st)) << st.ToString(); +} + +// report result, don't need to print args in here, just print the failure +::testing::AssertionResult CheckStatisticsInfo(const StatisticsInfo& expect, const StatisticsInfo& value) { + if (expect.idx_cnt_vec.size() != value.idx_cnt_vec.size()) { + return ::testing::AssertionFailure() + << "idx_cnt_vec size expect " << expect.idx_cnt_vec.size() << " but got " << value.idx_cnt_vec.size(); + } + for (size_t idx = 0; idx < expect.idx_cnt_vec.size(); idx++) { + if (expect.idx_cnt_vec[idx] != value.idx_cnt_vec[idx]) { + return ::testing::AssertionFailure() << "idx_cnt_vec[" << idx << "] expect " << expect.idx_cnt_vec[idx] + << " but got " << value.idx_cnt_vec[idx]; + } + } + if (expect.record_byte_size != value.record_byte_size) { + return ::testing::AssertionFailure() + << "record_byte_size expect " << expect.record_byte_size << " but got " << value.record_byte_size; + } + if (expect.idx_byte_size != value.idx_byte_size) { + return ::testing::AssertionFailure() + << "idx_byte_size expect " << expect.idx_byte_size << " but got " << value.idx_byte_size; + } + return ::testing::AssertionSuccess(); +} + +// helper +::testing::AssertionResult CheckStatisticsInfo(std::initializer_list vec, uint64_t idx_byte_size, + uint64_t record_byte_size, const StatisticsInfo& value) { + StatisticsInfo info(0); // overwrite by set idx_cnt_vec + info.idx_cnt_vec = vec; + info.idx_byte_size = idx_byte_size; + info.record_byte_size = record_byte_size; + return CheckStatisticsInfo(info, value); +} + +StatisticsInfo CreateStatisticsInfo(uint64_t idx_cnt, uint64_t idx_byte_size, uint64_t record_byte_size) { + StatisticsInfo info(1); + info.idx_cnt_vec[0] = idx_cnt; + info.idx_byte_size = idx_byte_size; + info.record_byte_size = record_byte_size; + return info; +} + +// TODO(hw): gc multi idx has bug, fix later +// TEST_F(IOTSegmentTest, TestGc4Head) { +// IOTSegment segment(8); +// Slice pk("PK"); +// segment.Put(pk, 9768, "test1", 5); +// segment.Put(pk, 9769, "test2", 5); +// StatisticsInfo gc_info(1); +// segment.Gc4Head(1, &gc_info); +// CheckStatisticsInfo(CreateStatisticsInfo(1, 0, GetRecordSize(5)), gc_info); +// Ticket ticket; +// std::unique_ptr it(segment.NewIterator(pk, ticket, type::CompressType::kNoCompress)); +// it->Seek(9769); +// ASSERT_TRUE(it->Valid()); +// ASSERT_EQ(9769, (int64_t)it->GetKey()); +// ::openmldb::base::Slice value = it->GetValue(); +// std::string result(value.data(), value.size()); +// ASSERT_EQ("test2", result); +// it->Next(); +// ASSERT_FALSE(it->Valid()); +// } + +TEST_F(IOTSegmentTest, TestGc4TTL) { + // cidx segment won't execute gc, gc will be done in iot gc + // and multi idx gc `GcAllType` has bug, skip test it + { + std::vector idx_vec = {1}; + std::vector idx_type = {common::IndexType::kClustered}; + auto segment = std::make_unique(8, idx_vec, idx_type); + Slice pk("test1"); + std::string value = "test0"; + auto cblk = new DataBlock(1, value.c_str(), value.size()); // 1 clustered + 1 covering, hard copy + auto sblk = new DataBlock(1, value.c_str(), value.size()); // 1 secondary, fake value, hard copy + ASSERT_TRUE(segment->Put(pk, {{1, 100}}, cblk, sblk)); + // ref iot gc SchedGCByDelete + StatisticsInfo statistics_info(segment->GetTsCnt()); + segment->IncrGcVersion(); + segment->GcFreeList(&statistics_info); + segment->ExecuteGc({{1, {1, 0, TTLType::kAbsoluteTime}}}, &statistics_info, segment->ClusteredTs()); + ASSERT_TRUE(CheckStatisticsInfo({0}, 0, 0, statistics_info)); + } + { + std::vector idx_vec = {1}; + std::vector idx_type = {common::IndexType::kSecondary}; + auto segment = std::make_unique(8, idx_vec, idx_type); + Slice pk("test1"); + std::string value = "test0"; + auto cblk = new DataBlock(1, value.c_str(), value.size()); // 1 clustered + 1 covering, hard copy + // execute gc will delete it + auto sblk = new DataBlock(1, value.c_str(), value.size()); // 1 secondary, fake value, hard copy + ASSERT_TRUE(segment->Put(pk, {{1, 100}}, cblk, sblk)); + // ref iot gc SchedGCByDelete + StatisticsInfo statistics_info(segment->GetTsCnt()); + segment->IncrGcVersion(); // 1 + segment->GcFreeList(&statistics_info); + segment->ExecuteGc({{1, {1, 0, TTLType::kAbsoluteTime}}}, &statistics_info, segment->ClusteredTs()); + // secondary will gc, but idx_byte_size is 0(GcFreeList change it) + ASSERT_TRUE(CheckStatisticsInfo({1}, 0, GetRecordSize(5), statistics_info)); + + segment->IncrGcVersion(); // 2 + segment->GcFreeList(&statistics_info); // empty + ASSERT_TRUE(CheckStatisticsInfo({1}, 0, GetRecordSize(5), statistics_info)); + segment->IncrGcVersion(); // delta default is 2, version should >=2, and node_cache free version should >= 3 + segment->GcFreeList(&statistics_info); + // don't know why 197 + ASSERT_TRUE(CheckStatisticsInfo({1}, 197, GetRecordSize(5), statistics_info)); + } +} + +// TEST_F(IOTSegmentTest, TestGc4TTLAndHead) { +// IOTSegment segment(8); +// segment.Put("PK1", 9766, "test1", 5); +// segment.Put("PK1", 9767, "test2", 5); +// segment.Put("PK1", 9768, "test3", 5); +// segment.Put("PK1", 9769, "test4", 5); +// segment.Put("PK2", 9765, "test1", 5); +// segment.Put("PK2", 9766, "test2", 5); +// segment.Put("PK2", 9767, "test3", 5); +// StatisticsInfo gc_info(1); +// // Gc4TTLAndHead only change gc_info.vec[0], check code +// // no expire +// segment.Gc4TTLAndHead(0, 0, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({0}, 0, 0, gc_info)); +// // no lat expire, so all records won't be deleted +// segment.Gc4TTLAndHead(9999, 0, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({0}, 0, 0, gc_info)); +// // no abs expire, so all records won't be deleted +// segment.Gc4TTLAndHead(0, 3, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({0}, 0, 0, gc_info)); +// // current_time > expire_time means not expired, so == is outdate and lat 2, so `9765` should be deleted +// segment.Gc4TTLAndHead(9765, 2, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({1}, 0, GetRecordSize(5), gc_info)); +// // gc again, no record expired, info won't update +// segment.Gc4TTLAndHead(9765, 2, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({1}, 0, GetRecordSize(5), gc_info)); +// // new info +// gc_info.Reset(); +// // time <= 9770 is abs expired, but lat 1, so just 1 record per key left, 4 deleted +// segment.Gc4TTLAndHead(9770, 1, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({4}, 0, 4 * GetRecordSize(5), gc_info)); +// uint64_t cnt = 0; +// ASSERT_EQ(0, segment.GetCount("PK1", cnt)); +// ASSERT_EQ(1, cnt); +// ASSERT_EQ(0, segment.GetCount("PK2", cnt)); +// ASSERT_EQ(1, cnt); +// } + +// TEST_F(IOTSegmentTest, TestGc4TTLOrHead) { +// IOTSegment segment(8); +// segment.Put("PK1", 9766, "test1", 5); +// segment.Put("PK1", 9767, "test2", 5); +// segment.Put("PK1", 9768, "test3", 5); +// segment.Put("PK1", 9769, "test4", 5); +// segment.Put("PK2", 9765, "test1", 5); +// segment.Put("PK2", 9766, "test2", 5); +// segment.Put("PK2", 9767, "test3", 5); +// StatisticsInfo gc_info(1); +// // no expire +// segment.Gc4TTLOrHead(0, 0, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({0}, 0, 0, gc_info)); +// // all record <= 9765 should be deleted, no matter the lat expire +// segment.Gc4TTLOrHead(9765, 0, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({1}, 0, GetRecordSize(5), gc_info)); +// gc_info.Reset(); +// // even abs no expire, only lat 3 per key +// segment.Gc4TTLOrHead(0, 3, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({1}, 0, GetRecordSize(5), gc_info)); +// gc_info.Reset(); +// segment.Gc4TTLOrHead(9765, 3, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({0}, 0, 0, gc_info)); +// segment.Gc4TTLOrHead(9766, 2, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({2}, 0, 2 * GetRecordSize(5), gc_info)); +// gc_info.Reset(); +// segment.Gc4TTLOrHead(9770, 1, &gc_info); +// ASSERT_TRUE(CheckStatisticsInfo({3}, 0, 3 * GetRecordSize(5), gc_info)); +// } + +// TEST_F(IOTSegmentTest, TestStat) { +// IOTSegment segment(8); +// segment.Put("PK", 9768, "test1", 5); +// segment.Put("PK", 9769, "test2", 5); +// ASSERT_EQ(2, (int64_t)segment.GetIdxCnt()); +// ASSERT_EQ(1, (int64_t)segment.GetPkCnt()); +// StatisticsInfo gc_info(1); +// segment.Gc4TTL(9765, &gc_info); +// ASSERT_EQ(0, gc_info.GetTotalCnt()); +// gc_info.Reset(); +// segment.Gc4TTL(9768, &gc_info); +// ASSERT_EQ(1, (int64_t)segment.GetIdxCnt()); +// ASSERT_EQ(1, gc_info.GetTotalCnt()); +// segment.Gc4TTL(9770, &gc_info); +// ASSERT_EQ(2, gc_info.GetTotalCnt()); +// ASSERT_EQ(0, (int64_t)segment.GetIdxCnt()); +// } + +// TEST_F(IOTSegmentTest, GetTsIdx) { +// std::vector ts_idx_vec = {1, 3, 5}; +// IOTSegment segment(8, ts_idx_vec); +// ASSERT_EQ(3, (int64_t)segment.GetTsCnt()); +// uint32_t real_idx = UINT32_MAX; +// ASSERT_EQ(-1, segment.GetTsIdx(0, real_idx)); +// ASSERT_EQ(0, segment.GetTsIdx(1, real_idx)); +// ASSERT_EQ(0, (int64_t)real_idx); +// ASSERT_EQ(-1, segment.GetTsIdx(2, real_idx)); +// ASSERT_EQ(0, segment.GetTsIdx(3, real_idx)); +// ASSERT_EQ(1, (int64_t)real_idx); +// ASSERT_EQ(-1, segment.GetTsIdx(4, real_idx)); +// ASSERT_EQ(0, segment.GetTsIdx(5, real_idx)); +// ASSERT_EQ(2, (int64_t)real_idx); +// } + +// int GetCount(IOTSegment* segment, int idx) { +// int count = 0; +// std::unique_ptr pk_it(segment->GetKeyEntries()->NewIterator()); +// if (!pk_it) { +// return 0; +// } +// uint32_t real_idx = idx; +// segment->GetTsIdx(idx, real_idx); +// pk_it->SeekToFirst(); +// while (pk_it->Valid()) { +// KeyEntry* entry = nullptr; +// if (segment->GetTsCnt() > 1) { +// entry = reinterpret_cast(pk_it->GetValue())[real_idx]; +// } else { +// entry = reinterpret_cast(pk_it->GetValue()); +// } +// std::unique_ptr ts_it(entry->entries.NewIterator()); +// ts_it->SeekToFirst(); +// while (ts_it->Valid()) { +// count++; +// ts_it->Next(); +// } +// pk_it->Next(); +// } +// return count; +// } + +// TEST_F(IOTSegmentTest, ReleaseAndCount) { +// std::vector ts_idx_vec = {1, 3}; +// IOTSegment segment(8, ts_idx_vec); +// ASSERT_EQ(2, (int64_t)segment.GetTsCnt()); +// for (int i = 0; i < 100; i++) { +// std::string key = "key" + std::to_string(i); +// uint64_t ts = 1669013677221000; +// for (int j = 0; j < 2; j++) { +// DataBlock* data = new DataBlock(2, key.c_str(), key.length()); +// std::map ts_map = {{1, ts + j}, {3, ts + j}}; +// segment.Put(Slice(key), ts_map, data); +// } +// } +// ASSERT_EQ(200, GetCount(&segment, 1)); +// ASSERT_EQ(200, GetCount(&segment, 3)); +// StatisticsInfo gc_info(1); +// segment.ReleaseAndCount({1}, &gc_info); +// ASSERT_EQ(0, GetCount(&segment, 1)); +// ASSERT_EQ(200, GetCount(&segment, 3)); +// segment.ReleaseAndCount(&gc_info); +// ASSERT_EQ(0, GetCount(&segment, 1)); +// ASSERT_EQ(0, GetCount(&segment, 3)); +// } + +// TEST_F(IOTSegmentTest, ReleaseAndCountOneTs) { +// IOTSegment segment(8); +// for (int i = 0; i < 100; i++) { +// std::string key = "key" + std::to_string(i); +// uint64_t ts = 1669013677221000; +// for (int j = 0; j < 2; j++) { +// segment.Put(Slice(key), ts + j, key.c_str(), key.size()); +// } +// } +// StatisticsInfo gc_info(1); +// ASSERT_EQ(200, GetCount(&segment, 0)); +// segment.ReleaseAndCount(&gc_info); +// ASSERT_EQ(0, GetCount(&segment, 0)); +// } + +// TEST_F(IOTSegmentTest, TestDeleteRange) { +// IOTSegment segment(8); +// for (int idx = 0; idx < 10; idx++) { +// std::string key = absl::StrCat("key", idx); +// std::string value = absl::StrCat("value", idx); +// uint64_t ts = 1000; +// for (int i = 0; i < 10; i++) { +// segment.Put(Slice(key), ts + i, value.data(), 6); +// } +// } +// ASSERT_EQ(100, GetCount(&segment, 0)); +// std::string pk = "key2"; +// Ticket ticket; +// std::unique_ptr it(segment.NewIterator(pk, ticket, type::CompressType::kNoCompress)); +// it->Seek(1005); +// ASSERT_TRUE(it->Valid() && it->GetKey() == 1005); +// ASSERT_TRUE(segment.Delete(std::nullopt, pk, 1005, 1004)); +// ASSERT_EQ(99, GetCount(&segment, 0)); +// it->Seek(1005); +// ASSERT_FALSE(it->Valid() && it->GetKey() == 1005); +// ASSERT_TRUE(segment.Delete(std::nullopt, pk, 1005, std::nullopt)); +// ASSERT_EQ(94, GetCount(&segment, 0)); +// it->Seek(1005); +// ASSERT_FALSE(it->Valid()); +// pk = "key3"; +// ASSERT_TRUE(segment.Delete(std::nullopt, pk)); +// pk = "key4"; +// ASSERT_TRUE(segment.Delete(std::nullopt, pk, 1005, 1001)); +// ASSERT_EQ(80, GetCount(&segment, 0)); +// segment.IncrGcVersion(); +// segment.IncrGcVersion(); +// StatisticsInfo gc_info(1); +// segment.GcFreeList(&gc_info); +// CheckStatisticsInfo(CreateStatisticsInfo(20, 1012, 20 * (6 + sizeof(DataBlock))), gc_info); +// } + +// TEST_F(IOTSegmentTest, PutIfAbsent) { +// { +// IOTSegment segment(8); // so ts_cnt_ == 1 +// // check all time == false +// segment.Put("PK", 1, "test1", 5, true); +// segment.Put("PK", 1, "test2", 5, true); // even key&time is the same, different value means different record +// ASSERT_EQ(2, (int64_t)segment.GetIdxCnt()); +// ASSERT_EQ(1, (int64_t)segment.GetPkCnt()); +// segment.Put("PK", 2, "test3", 5, true); +// segment.Put("PK", 2, "test4", 5, true); +// segment.Put("PK", 3, "test5", 5, true); +// segment.Put("PK", 3, "test6", 5, true); +// ASSERT_EQ(6, (int64_t)segment.GetIdxCnt()); +// // insert exists rows +// segment.Put("PK", 2, "test3", 5, true); +// segment.Put("PK", 1, "test1", 5, true); +// segment.Put("PK", 1, "test2", 5, true); +// segment.Put("PK", 3, "test6", 5, true); +// ASSERT_EQ(6, (int64_t)segment.GetIdxCnt()); +// // new rows +// segment.Put("PK", 2, "test7", 5, true); +// ASSERT_EQ(7, (int64_t)segment.GetIdxCnt()); +// segment.Put("PK", 0, "test8", 5, true); // seek to last, next is empty +// ASSERT_EQ(8, (int64_t)segment.GetIdxCnt()); +// } + +// { +// // support when ts_cnt_ != 1 too +// std::vector ts_idx_vec = {1, 3}; +// IOTSegment segment(8, ts_idx_vec); +// ASSERT_EQ(2, (int64_t)segment.GetTsCnt()); +// std::string key = "PK"; +// uint64_t ts = 1669013677221000; +// // the same ts +// for (int j = 0; j < 2; j++) { +// DataBlock* data = new DataBlock(2, key.c_str(), key.length()); +// std::map ts_map = {{1, ts}, {3, ts}}; +// segment.Put(Slice(key), ts_map, data, true); +// } +// ASSERT_EQ(1, GetCount(&segment, 1)); +// ASSERT_EQ(1, GetCount(&segment, 3)); +// } + +// { +// // put ts_map contains DEFAULT_TS_COL_ID +// std::vector ts_idx_vec = {DEFAULT_TS_COL_ID}; +// IOTSegment segment(8, ts_idx_vec); +// ASSERT_EQ(1, (int64_t)segment.GetTsCnt()); +// std::string key = "PK"; +// std::map ts_map = {{DEFAULT_TS_COL_ID, 100}}; // cur time == 100 +// auto* block = new DataBlock(1, "test1", 5); +// segment.Put(Slice(key), ts_map, block, true); +// ASSERT_EQ(1, GetCount(&segment, DEFAULT_TS_COL_ID)); +// ts_map = {{DEFAULT_TS_COL_ID, 200}}; +// block = new DataBlock(1, "test1", 5); +// segment.Put(Slice(key), ts_map, block, true); +// ASSERT_EQ(1, GetCount(&segment, DEFAULT_TS_COL_ID)); +// } + +// { +// // put ts_map contains DEFAULT_TS_COL_ID +// std::vector ts_idx_vec = {DEFAULT_TS_COL_ID, 1, 3}; +// IOTSegment segment(8, ts_idx_vec); +// ASSERT_EQ(3, (int64_t)segment.GetTsCnt()); +// std::string key = "PK"; +// std::map ts_map = {{DEFAULT_TS_COL_ID, 100}}; // cur time == 100 +// auto* block = new DataBlock(1, "test1", 5); +// segment.Put(Slice(key), ts_map, block, true); +// ASSERT_EQ(1, GetCount(&segment, DEFAULT_TS_COL_ID)); +// ts_map = {{DEFAULT_TS_COL_ID, 200}}; +// block = new DataBlock(1, "test1", 5); +// segment.Put(Slice(key), ts_map, block, true); +// ASSERT_EQ(1, GetCount(&segment, DEFAULT_TS_COL_ID)); +// } +// } + +} // namespace storage +} // namespace openmldb + +int main(int argc, char** argv) { + ::openmldb::base::SetLogLevel(INFO); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/storage/key_entry.cc b/src/storage/key_entry.cc index 2713510f16c..8af33e8fc70 100644 --- a/src/storage/key_entry.cc +++ b/src/storage/key_entry.cc @@ -36,7 +36,7 @@ void KeyEntry::Release(uint32_t idx, StatisticsInfo* statistics_info) { if (node->GetValue()->dim_cnt_down > 1) { node->GetValue()->dim_cnt_down--; } else { - DEBUGLOG("delele data block for key %lu", node->GetKey()); + VLOG(1) << "delete data block for key " << node->GetKey(); statistics_info->record_byte_size += GetRecordSize(node->GetValue()->size); delete node->GetValue(); } diff --git a/src/storage/mem_table.cc b/src/storage/mem_table.cc index 023974e3c6b..910fdc06e4d 100644 --- a/src/storage/mem_table.cc +++ b/src/storage/mem_table.cc @@ -17,6 +17,7 @@ #include "storage/mem_table.h" #include + #include #include @@ -26,8 +27,8 @@ #include "common/timer.h" #include "gflags/gflags.h" #include "schema/index_util.h" -#include "storage/record.h" #include "storage/mem_table_iterator.h" +#include "storage/record.h" DECLARE_uint32(skiplist_max_height); DECLARE_uint32(skiplist_max_height); @@ -54,7 +55,7 @@ MemTable::MemTable(const ::openmldb::api::TableMeta& table_meta) : Table(table_meta.storage_mode(), table_meta.name(), table_meta.tid(), table_meta.pid(), 0, true, 60 * 1000, std::map(), ::openmldb::type::TTLType::kAbsoluteTime, ::openmldb::type::CompressType::kNoCompress), - segments_(MAX_INDEX_NUM, nullptr) { + segments_(MAX_INDEX_NUM, nullptr) { seg_cnt_ = 8; enable_gc_ = true; segment_released_ = false; @@ -80,7 +81,7 @@ MemTable::~MemTable() { PDLOG(INFO, "drop memtable. tid %u pid %u", id_, pid_); } -bool MemTable::Init() { +bool MemTable::InitMeta() { key_entry_max_height_ = FLAGS_key_entry_max_height; if (!InitFromMeta()) { return false; @@ -88,21 +89,33 @@ bool MemTable::Init() { if (table_meta_->seg_cnt() > 0) { seg_cnt_ = table_meta_->seg_cnt(); } + return true; +} + +uint32_t MemTable::KeyEntryMaxHeight(const std::shared_ptr& inner_idx) { uint32_t global_key_entry_max_height = 0; if (table_meta_->has_key_entry_max_height() && table_meta_->key_entry_max_height() <= FLAGS_skiplist_max_height && table_meta_->key_entry_max_height() > 0) { global_key_entry_max_height = table_meta_->key_entry_max_height(); } + if (global_key_entry_max_height > 0) { + return global_key_entry_max_height; + } else { + return inner_idx->GetKeyEntryMaxHeight(FLAGS_absolute_default_skiplist_height, + FLAGS_latest_default_skiplist_height); + } +} +bool MemTable::Init() { + if (!InitMeta()) { + LOG(WARNING) << "init meta failed. tid " << id_ << " pid " << pid_; + return false; + } + auto inner_indexs = table_index_.GetAllInnerIndex(); for (uint32_t i = 0; i < inner_indexs->size(); i++) { const std::vector& ts_vec = inner_indexs->at(i)->GetTsIdx(); - uint32_t cur_key_entry_max_height = 0; - if (global_key_entry_max_height > 0) { - cur_key_entry_max_height = global_key_entry_max_height; - } else { - cur_key_entry_max_height = inner_indexs->at(i)->GetKeyEntryMaxHeight(FLAGS_absolute_default_skiplist_height, - FLAGS_latest_default_skiplist_height); - } + uint32_t cur_key_entry_max_height = KeyEntryMaxHeight(inner_indexs->at(i)); + Segment** seg_arr = new Segment*[seg_cnt_]; if (!ts_vec.empty()) { for (uint32_t j = 0; j < seg_cnt_; j++) { @@ -226,10 +239,8 @@ absl::Status MemTable::Put(uint64_t time, const std::string& value, const Dimens } bool MemTable::Delete(const ::openmldb::api::LogEntry& entry) { - std::optional start_ts = entry.has_ts() ? std::optional{entry.ts()} - : std::nullopt; - std::optional end_ts = entry.has_end_ts() ? std::optional{entry.end_ts()} - : std::nullopt; + std::optional start_ts = entry.has_ts() ? std::optional{entry.ts()} : std::nullopt; + std::optional end_ts = entry.has_end_ts() ? std::optional{entry.end_ts()} : std::nullopt; if (entry.dimensions_size() > 0) { for (const auto& dimension : entry.dimensions()) { if (!Delete(dimension.idx(), dimension.key(), start_ts, end_ts)) { @@ -259,8 +270,8 @@ bool MemTable::Delete(const ::openmldb::api::LogEntry& entry) { return true; } -bool MemTable::Delete(uint32_t idx, const std::string& key, - const std::optional& start_ts, const std::optional& end_ts) { +bool MemTable::Delete(uint32_t idx, const std::string& key, const std::optional& start_ts, + const std::optional& end_ts) { auto index_def = GetIndex(idx); if (!index_def || !index_def->IsReady()) { return false; @@ -336,7 +347,7 @@ void MemTable::SchedGc() { for (uint32_t k = 0; k < seg_cnt_; k++) { if (segments_[i][k] != nullptr) { StatisticsInfo statistics_info(segments_[i][k]->GetTsCnt()); - if (real_index.size() == 1 || deleting_pos.size() + deleted_num == real_index.size()) { + if (real_index.size() == 1 || deleting_pos.size() + deleted_num == real_index.size()) { segments_[i][k]->ReleaseAndCount(&statistics_info); } else { segments_[i][k]->ReleaseAndCount(deleting_pos, &statistics_info); @@ -377,8 +388,8 @@ void MemTable::SchedGc() { } consumed = ::baidu::common::timer::get_micros() - consumed; record_byte_size_.fetch_sub(gc_record_byte_size, std::memory_order_relaxed); - PDLOG(INFO, "gc finished, gc_idx_cnt %lu, consumed %lu ms for table %s tid %u pid %u", - gc_idx_cnt, consumed / 1000, name_.c_str(), id_, pid_); + PDLOG(INFO, "gc finished, gc_idx_cnt %lu, consumed %lu ms for table %s tid %u pid %u", gc_idx_cnt, consumed / 1000, + name_.c_str(), id_, pid_); UpdateTTL(); } @@ -620,18 +631,25 @@ bool MemTable::GetRecordIdxCnt(uint32_t idx, uint64_t** stat, uint32_t* size) { } bool MemTable::AddIndexToTable(const std::shared_ptr& index_def) { - std::vector ts_vec = { index_def->GetTsColumn()->GetId() }; + std::vector ts_vec = {index_def->GetTsColumn()->GetId()}; uint32_t inner_id = index_def->GetInnerPos(); Segment** seg_arr = new Segment*[seg_cnt_]; for (uint32_t j = 0; j < seg_cnt_; j++) { seg_arr[j] = new Segment(FLAGS_absolute_default_skiplist_height, ts_vec); PDLOG(INFO, "init %u, %u segment. height %u, ts col num %u. tid %u pid %u", inner_id, j, - FLAGS_absolute_default_skiplist_height, ts_vec.size(), id_, pid_); + FLAGS_absolute_default_skiplist_height, ts_vec.size(), id_, pid_); } segments_[inner_id] = seg_arr; return true; } +uint32_t MemTable::SegIdx(const std::string& pk) { + if (seg_cnt_ > 1) { + return ::openmldb::base::hash(pk.c_str(), pk.length(), SEED) % seg_cnt_; + } + return 0; +} + ::hybridse::vm::WindowIterator* MemTable::NewWindowIterator(uint32_t index) { std::shared_ptr index_def = table_index_.GetIndex(index); if (!index_def || !index_def->IsReady()) { @@ -651,8 +669,8 @@ ::hybridse::vm::WindowIterator* MemTable::NewWindowIterator(uint32_t index) { if (ts_col) { ts_idx = ts_col->GetId(); } - return new MemTableKeyIterator(segments_[real_idx], seg_cnt_, ttl->ttl_type, - expire_time, expire_cnt, ts_idx, GetCompressType()); + return new MemTableKeyIterator(segments_[real_idx], seg_cnt_, ttl->ttl_type, expire_time, expire_cnt, ts_idx, + GetCompressType()); } TraverseIterator* MemTable::NewTraverseIterator(uint32_t index) { @@ -671,11 +689,11 @@ TraverseIterator* MemTable::NewTraverseIterator(uint32_t index) { uint32_t real_idx = index_def->GetInnerPos(); auto ts_col = index_def->GetTsColumn(); if (ts_col) { - return new MemTableTraverseIterator(segments_[real_idx], seg_cnt_, ttl->ttl_type, - expire_time, expire_cnt, ts_col->GetId(), GetCompressType()); + return new MemTableTraverseIterator(segments_[real_idx], seg_cnt_, ttl->ttl_type, expire_time, expire_cnt, + ts_col->GetId(), GetCompressType()); } - return new MemTableTraverseIterator(segments_[real_idx], seg_cnt_, ttl->ttl_type, - expire_time, expire_cnt, 0, GetCompressType()); + return new MemTableTraverseIterator(segments_[real_idx], seg_cnt_, ttl->ttl_type, expire_time, expire_cnt, 0, + GetCompressType()); } bool MemTable::GetBulkLoadInfo(::openmldb::api::BulkLoadInfoResponse* response) { diff --git a/src/storage/mem_table.h b/src/storage/mem_table.h index 694203c3e40..c85ffd12da4 100644 --- a/src/storage/mem_table.h +++ b/src/storage/mem_table.h @@ -54,10 +54,10 @@ class MemTable : public Table { absl::Status Put(uint64_t time, const std::string& value, const Dimensions& dimensions, bool put_if_absent) override; - bool GetBulkLoadInfo(::openmldb::api::BulkLoadInfoResponse* response); + virtual bool GetBulkLoadInfo(::openmldb::api::BulkLoadInfoResponse* response); - bool BulkLoad(const std::vector& data_blocks, - const ::google::protobuf::RepeatedPtrField<::openmldb::api::BulkLoadIndex>& indexes); + virtual bool BulkLoad(const std::vector& data_blocks, + const ::google::protobuf::RepeatedPtrField<::openmldb::api::BulkLoadIndex>& indexes); bool Delete(const ::openmldb::api::LogEntry& entry) override; @@ -68,7 +68,7 @@ class MemTable : public Table { TraverseIterator* NewTraverseIterator(uint32_t index) override; - ::hybridse::vm::WindowIterator* NewWindowIterator(uint32_t index); + ::hybridse::vm::WindowIterator* NewWindowIterator(uint32_t index) override; // release all memory allocated uint64_t Release(); @@ -104,15 +104,26 @@ class MemTable : public Table { protected: bool AddIndexToTable(const std::shared_ptr& index_def) override; + uint32_t SegIdx(const std::string& pk); + + Segment* GetSegment(uint32_t real_idx, uint32_t seg_idx) { + // TODO(hw): protect + return segments_[real_idx][seg_idx]; + } + Segment** GetSegments(uint32_t real_idx) { return segments_[real_idx]; } + + bool InitMeta(); + uint32_t KeyEntryMaxHeight(const std::shared_ptr& inner_idx); + private: bool CheckAbsolute(const TTLSt& ttl, uint64_t ts); bool CheckLatest(uint32_t index_id, const std::string& key, uint64_t ts); - bool Delete(uint32_t idx, const std::string& key, - const std::optional& start_ts, const std::optional& end_ts); + bool Delete(uint32_t idx, const std::string& key, const std::optional& start_ts, + const std::optional& end_ts); - private: + protected: uint32_t seg_cnt_; std::vector segments_; std::atomic enable_gc_; diff --git a/src/storage/mem_table_iterator.cc b/src/storage/mem_table_iterator.cc index 22cd7964640..f508d404af7 100644 --- a/src/storage/mem_table_iterator.cc +++ b/src/storage/mem_table_iterator.cc @@ -138,7 +138,7 @@ void MemTableKeyIterator::Next() { NextPK(); } -::hybridse::vm::RowIterator* MemTableKeyIterator::GetRawValue() { +TimeEntries::Iterator* MemTableKeyIterator::GetTimeIter() { TimeEntries::Iterator* it = nullptr; if (segments_[seg_idx_]->GetTsCnt() > 1) { KeyEntry* entry = ((KeyEntry**)pk_it_->GetValue())[ts_idx_]; // NOLINT @@ -150,6 +150,11 @@ ::hybridse::vm::RowIterator* MemTableKeyIterator::GetRawValue() { ticket_.Push((KeyEntry*)pk_it_->GetValue()); // NOLINT } it->SeekToFirst(); + return it; +} + +::hybridse::vm::RowIterator* MemTableKeyIterator::GetRawValue() { + TimeEntries::Iterator* it = GetTimeIter(); return new MemTableWindowIterator(it, ttl_type_, expire_time_, expire_cnt_, compress_type_); } diff --git a/src/storage/mem_table_iterator.h b/src/storage/mem_table_iterator.h index 4b3b2514824..427cdc09100 100644 --- a/src/storage/mem_table_iterator.h +++ b/src/storage/mem_table_iterator.h @@ -18,6 +18,7 @@ #include #include + #include "storage/segment.h" #include "vm/catalog.h" @@ -27,9 +28,12 @@ namespace storage { class MemTableWindowIterator : public ::hybridse::vm::RowIterator { public: MemTableWindowIterator(TimeEntries::Iterator* it, ::openmldb::storage::TTLType ttl_type, uint64_t expire_time, - uint64_t expire_cnt, type::CompressType compress_type) - : it_(it), record_idx_(1), expire_value_(expire_time, expire_cnt, ttl_type), - row_(), compress_type_(compress_type) {} + uint64_t expire_cnt, type::CompressType compress_type) + : it_(it), + record_idx_(1), + expire_value_(expire_time, expire_cnt, ttl_type), + row_(), + compress_type_(compress_type) {} ~MemTableWindowIterator(); @@ -59,8 +63,7 @@ class MemTableWindowIterator : public ::hybridse::vm::RowIterator { class MemTableKeyIterator : public ::hybridse::vm::WindowIterator { public: MemTableKeyIterator(Segment** segments, uint32_t seg_cnt, ::openmldb::storage::TTLType ttl_type, - uint64_t expire_time, uint64_t expire_cnt, uint32_t ts_index, - type::CompressType compress_type); + uint64_t expire_time, uint64_t expire_cnt, uint32_t ts_index, type::CompressType compress_type); ~MemTableKeyIterator() override; @@ -77,10 +80,13 @@ class MemTableKeyIterator : public ::hybridse::vm::WindowIterator { const hybridse::codec::Row GetKey() override; + protected: + TimeEntries::Iterator* GetTimeIter(); + private: void NextPK(); - private: + protected: Segment** segments_; uint32_t const seg_cnt_; uint32_t seg_idx_; @@ -97,10 +103,10 @@ class MemTableKeyIterator : public ::hybridse::vm::WindowIterator { class MemTableTraverseIterator : public TraverseIterator { public: MemTableTraverseIterator(Segment** segments, uint32_t seg_cnt, ::openmldb::storage::TTLType ttl_type, - uint64_t expire_time, uint64_t expire_cnt, uint32_t ts_index, - type::CompressType compress_type); + uint64_t expire_time, uint64_t expire_cnt, uint32_t ts_index, + type::CompressType compress_type); ~MemTableTraverseIterator() override; - inline bool Valid() override; + bool Valid() override; void Next() override; void NextPK() override; void Seek(const std::string& key, uint64_t time) override; diff --git a/src/storage/node_cache.cc b/src/storage/node_cache.cc index 0f1286494e8..766dfe78be3 100644 --- a/src/storage/node_cache.cc +++ b/src/storage/node_cache.cc @@ -79,6 +79,7 @@ void NodeCache::Free(uint64_t version, StatisticsInfo* gc_info) { node1 = key_entry_node_list_.Split(version); node2 = value_node_list_.Split(version); } + DLOG(INFO) << "free version " << version << ", node1 " << node1 << ", node2 " << node2; while (node1) { auto entry_node_list = node1->GetValue(); for (auto& entry_node : *entry_node_list) { @@ -113,11 +114,11 @@ void NodeCache::FreeNode(uint32_t idx, base::Node* node, S } gc_info->IncrIdxCnt(idx); gc_info->idx_byte_size += GetRecordTsIdxSize(node->Height()); - DLOG(INFO) << "delete key " << node->GetKey() << " with height " << node->Height(); + VLOG(1) << "delete key " << node->GetKey() << " with height " << (unsigned int)node->Height(); if (node->GetValue()->dim_cnt_down > 1) { node->GetValue()->dim_cnt_down--; } else { - DLOG(INFO) << "delele data block for key " << node->GetKey(); + VLOG(1) << "delete data block for key " << node->GetKey(); gc_info->record_byte_size += GetRecordSize(node->GetValue()->size); delete node->GetValue(); } diff --git a/src/storage/record.h b/src/storage/record.h index b3b06611f90..a5ab3f651ff 100644 --- a/src/storage/record.h +++ b/src/storage/record.h @@ -18,8 +18,10 @@ #define SRC_STORAGE_RECORD_H_ #include -#include "base/slice.h" + +#include "absl/strings/str_cat.h" #include "base/skiplist.h" +#include "base/slice.h" #include "storage/key_entry.h" namespace openmldb { @@ -67,9 +69,7 @@ struct StatisticsInfo { } } - uint64_t GetIdxCnt(uint32_t idx) const { - return idx >= idx_cnt_vec.size() ? 0 : idx_cnt_vec[idx]; - } + uint64_t GetIdxCnt(uint32_t idx) const { return idx >= idx_cnt_vec.size() ? 0 : idx_cnt_vec[idx]; } uint64_t GetTotalCnt() const { uint64_t total_cnt = 0; @@ -79,6 +79,15 @@ struct StatisticsInfo { return total_cnt; } + std::string DebugString() { + std::string str; + absl::StrAppend(&str, "idx_byte_size: ", idx_byte_size, " record_byte_size: ", record_byte_size, " idx_cnt: "); + for (uint32_t i = 0; i < idx_cnt_vec.size(); i++) { + absl::StrAppend(&str, i, ":", idx_cnt_vec[i], " "); + } + return str; + } + std::vector idx_cnt_vec; uint64_t idx_byte_size = 0; uint64_t record_byte_size = 0; diff --git a/src/storage/schema.cc b/src/storage/schema.cc index 3250a047a8b..f8a9d4fa4a6 100644 --- a/src/storage/schema.cc +++ b/src/storage/schema.cc @@ -129,6 +129,21 @@ uint32_t InnerIndexSt::GetKeyEntryMaxHeight(uint32_t abs_max_height, uint32_t la return max_height; } +int64_t InnerIndexSt::ClusteredTsId() { + int64_t id = -1; + for (const auto& cur_index : index_) { + if (cur_index->IsClusteredIndex()) { + auto ts_col = cur_index->GetTsColumn(); + DLOG_ASSERT(ts_col) << "clustered index should have ts column, even auto gen"; + if (ts_col) { + id = ts_col->GetId(); + } + } + } + return id; +} + + TableIndex::TableIndex() { indexs_ = std::make_shared>>(); inner_indexs_ = std::make_shared>>(); @@ -195,7 +210,8 @@ int TableIndex::ParseFromMeta(const ::openmldb::api::TableMeta& table_meta) { } } } - uint32_t key_idx = 0; + + // pos == idx for (int pos = 0; pos < table_meta.column_key_size(); pos++) { const auto& column_key = table_meta.column_key(pos); std::string name = column_key.index_name(); @@ -209,8 +225,10 @@ int TableIndex::ParseFromMeta(const ::openmldb::api::TableMeta& table_meta) { for (const auto& cur_col_name : column_key.col_name()) { col_vec.push_back(*(col_map[cur_col_name])); } - auto index = std::make_shared(column_key.index_name(), key_idx, status, - ::openmldb::type::IndexType::kTimeSerise, col_vec); + // index type is optional + common::IndexType index_type = column_key.has_type() ? column_key.type() : common::IndexType::kCovering; + auto index = std::make_shared(column_key.index_name(), pos, status, + ::openmldb::type::IndexType::kTimeSerise, col_vec, index_type); if (!column_key.ts_name().empty()) { const std::string& ts_name = column_key.ts_name(); index->SetTsColumn(col_map[ts_name]); @@ -226,7 +244,6 @@ int TableIndex::ParseFromMeta(const ::openmldb::api::TableMeta& table_meta) { DLOG(WARNING) << "add index failed"; return -1; } - key_idx++; } } // add default dimension diff --git a/src/storage/schema.h b/src/storage/schema.h index 9edc6e54b2a..39ee5891700 100644 --- a/src/storage/schema.h +++ b/src/storage/schema.h @@ -24,6 +24,7 @@ #include #include +#include "base/glog_wrapper.h" #include "common/timer.h" #include "proto/name_server.pb.h" #include "proto/tablet.pb.h" @@ -35,13 +36,7 @@ static constexpr uint32_t MAX_INDEX_NUM = 200; static constexpr uint32_t DEFAULT_TS_COL_ID = UINT32_MAX; static constexpr const char* DEFAULT_TS_COL_NAME = "___default_ts___"; -enum TTLType { - kAbsoluteTime = 1, - kRelativeTime = 2, - kLatestTime = 3, - kAbsAndLat = 4, - kAbsOrLat = 5 -}; +enum TTLType { kAbsoluteTime = 1, kRelativeTime = 2, kLatestTime = 3, kAbsAndLat = 4, kAbsOrLat = 5 }; // ttl unit: millisecond struct TTLSt { @@ -147,8 +142,7 @@ struct TTLSt { }; struct ExpiredChecker { - ExpiredChecker(uint64_t abs, uint64_t lat, TTLType type) : - abs_expired_ttl(abs), lat_ttl(lat), ttl_type(type) {} + ExpiredChecker(uint64_t abs, uint64_t lat, TTLType type) : abs_expired_ttl(abs), lat_ttl(lat), ttl_type(type) {} bool IsExpired(uint64_t abs, uint32_t record_idx) const { switch (ttl_type) { case TTLType::kAbsoluteTime: @@ -234,6 +228,11 @@ class IndexDef { IndexDef(const std::string& name, uint32_t id, IndexStatus status); IndexDef(const std::string& name, uint32_t id, const IndexStatus& status, ::openmldb::type::IndexType type, const std::vector& column_idx_map); + IndexDef(const std::string& name, uint32_t id, const IndexStatus& status, ::openmldb::type::IndexType type, + const std::vector& column_idx_map, common::IndexType index_type) + : IndexDef(name, id, status, type, column_idx_map) { + index_type_ = index_type; + } const std::string& GetName() const { return name_; } inline const std::shared_ptr& GetTsColumn() const { return ts_column_; } void SetTsColumn(const std::shared_ptr& ts_column) { ts_column_ = ts_column; } @@ -250,15 +249,22 @@ class IndexDef { inline uint32_t GetInnerPos() const { return inner_pos_; } ::openmldb::common::ColumnKey GenColumnKey(); + common::IndexType GetIndexType() const { return index_type_; } + bool IsSecondaryIndex() { return index_type_ == common::IndexType::kSecondary; } + bool IsClusteredIndex() { return index_type_ == common::IndexType::kClustered; } + private: std::string name_; uint32_t index_id_; uint32_t inner_pos_; std::atomic status_; + // for compatible, type is only kTimeSerise ::openmldb::type::IndexType type_; std::vector columns_; std::shared_ptr ttl_st_; std::shared_ptr ts_column_; + // 0 covering, 1 clustered, 2 secondary, default 0 + common::IndexType index_type_ = common::IndexType::kCovering; }; class InnerIndexSt { @@ -270,11 +276,22 @@ class InnerIndexSt { ts_.push_back(ts_col->GetId()); } } + LOG_IF(DFATAL, ts_.size() != index_.size()) << "ts size not equal to index size"; } inline uint32_t GetId() const { return id_; } inline const std::vector& GetTsIdx() const { return ts_; } + // len(ts) == len(type) + inline std::vector GetTsIdxType() const { + std::vector ts_idx_type; + for (const auto& cur_index : index_) { + if (cur_index->GetTsColumn()) ts_idx_type.push_back(cur_index->GetIndexType()); + } + return ts_idx_type; + } inline const std::vector>& GetIndex() const { return index_; } uint32_t GetKeyEntryMaxHeight(uint32_t abs_max_height, uint32_t lat_max_height) const; + // -1 means no clustered idx in here, it's safe to cvt to uint32_t when id >= 0 + int64_t ClusteredTsId(); private: const uint32_t id_; diff --git a/src/storage/segment.cc b/src/storage/segment.cc index 6eb721d353c..18a47c961c4 100644 --- a/src/storage/segment.cc +++ b/src/storage/segment.cc @@ -175,6 +175,7 @@ bool Segment::PutUnlock(const Slice& key, uint64_t time, DataBlock* row, bool pu reinterpret_cast(entry)->count_.fetch_add(1, std::memory_order_relaxed); byte_size += GetRecordTsIdxSize(height); idx_byte_size_.fetch_add(byte_size, std::memory_order_relaxed); + DLOG(INFO) << "idx_byte_size_ " << idx_byte_size_ << " after add " << byte_size; return true; } @@ -251,6 +252,7 @@ bool Segment::Put(const Slice& key, const std::map& ts_map, D entry->count_.fetch_add(1, std::memory_order_relaxed); byte_size += GetRecordTsIdxSize(height); idx_byte_size_.fetch_add(byte_size, std::memory_order_relaxed); + DLOG(INFO) << "idx_byte_size_ " << idx_byte_size_ << " after add " << byte_size; idx_cnt_vec_[pos->second]->fetch_add(1, std::memory_order_relaxed); } return true; @@ -268,11 +270,13 @@ bool Segment::Delete(const std::optional& idx, const Slice& key) { entry_node = entries_->Remove(key); } if (entry_node != nullptr) { + DLOG(INFO) << "add key " << key.ToString() << " to node cache. version " << gc_version_; node_cache_.AddKeyEntryNode(gc_version_.load(std::memory_order_relaxed), entry_node); return true; } } else { base::Node* data_node = nullptr; + ::openmldb::base::Node* entry_node = nullptr; { std::lock_guard lock(mu_); void* entry_arr = nullptr; @@ -286,10 +290,24 @@ bool Segment::Delete(const std::optional& idx, const Slice& key) { uint64_t ts = it->GetKey(); data_node = key_entry->entries.Split(ts); } + bool is_empty = true; + for (uint32_t i = 0; i < ts_cnt_; i++) { + if (!reinterpret_cast(entry_arr)[i]->entries.IsEmpty()) { + is_empty = false; + break; + } + } + if (is_empty) { + entry_node = entries_->Remove(key); + } } if (data_node != nullptr) { node_cache_.AddValueNodeList(ts_idx, gc_version_.load(std::memory_order_relaxed), data_node); } + if (entry_node != nullptr) { + DLOG(INFO) << "add key " << key.ToString() << " to node cache. version " << gc_version_; + node_cache_.AddKeyEntryNode(gc_version_.load(std::memory_order_relaxed), entry_node); + } } return true; } @@ -313,12 +331,13 @@ bool Segment::GetTsIdx(const std::optional& idx, uint32_t* ts_idx) { return true; } -bool Segment::Delete(const std::optional& idx, const Slice& key, - uint64_t ts, const std::optional& end_ts) { +bool Segment::Delete(const std::optional& idx, const Slice& key, uint64_t ts, + const std::optional& end_ts) { uint32_t ts_idx = 0; if (!GetTsIdx(idx, &ts_idx)) { return false; } + void* entry = nullptr; if (entries_->Get(key, entry) < 0 || entry == nullptr) { return true; @@ -351,14 +370,33 @@ bool Segment::Delete(const std::optional& idx, const Slice& key, } } base::Node* data_node = nullptr; + base::Node* entry_node = nullptr; { std::lock_guard lock(mu_); data_node = key_entry->entries.Split(ts); - DLOG(INFO) << "entry " << key.ToString() << " split by " << ts; + DLOG(INFO) << "after delete, entry " << key.ToString() << " split by " << ts; + bool is_empty = true; + if (ts_cnt_ == 1) { + is_empty = key_entry->entries.IsEmpty(); + } else { + for (uint32_t i = 0; i < ts_cnt_; i++) { + if (!reinterpret_cast(entry)[i]->entries.IsEmpty()) { + is_empty = false; + break; + } + } + } + if (is_empty) { + entry_node = entries_->Remove(key); + } } if (data_node != nullptr) { node_cache_.AddValueNodeList(ts_idx, gc_version_.load(std::memory_order_relaxed), data_node); } + if (entry_node != nullptr) { + DLOG(INFO) << "add key " << key.ToString() << " to node cache. version " << gc_version_; + node_cache_.AddKeyEntryNode(gc_version_.load(std::memory_order_relaxed), entry_node); + } return true; } @@ -368,12 +406,13 @@ void Segment::FreeList(uint32_t ts_idx, ::openmldb::base::NodeIncrIdxCnt(ts_idx); ::openmldb::base::Node* tmp = node; idx_byte_size_.fetch_sub(GetRecordTsIdxSize(tmp->Height())); + DLOG(INFO) << "idx_byte_size_ " << idx_byte_size_ << " after sub " << GetRecordTsIdxSize(tmp->Height()); node = node->GetNextNoBarrier(0); - DEBUGLOG("delete key %lu with height %u", tmp->GetKey(), tmp->Height()); + VLOG(1) << "delete key " << tmp->GetKey() << " with height " << (unsigned int)tmp->Height(); if (tmp->GetValue()->dim_cnt_down > 1) { tmp->GetValue()->dim_cnt_down--; } else { - DEBUGLOG("delele data block for key %lu", tmp->GetKey()); + VLOG(1) << "delete data block for key " << tmp->GetKey(); statistics_info->record_byte_size += GetRecordSize(tmp->GetValue()->size); delete tmp->GetValue(); } @@ -387,12 +426,17 @@ void Segment::GcFreeList(StatisticsInfo* statistics_info) { return; } StatisticsInfo old = *statistics_info; + DLOG(INFO) << "cur " << old.DebugString(); uint64_t free_list_version = cur_version - FLAGS_gc_deleted_pk_version_delta; node_cache_.Free(free_list_version, statistics_info); + DLOG(INFO) << "after node cache free " << statistics_info->DebugString(); for (size_t idx = 0; idx < idx_cnt_vec_.size(); idx++) { idx_cnt_vec_[idx]->fetch_sub(statistics_info->GetIdxCnt(idx) - old.GetIdxCnt(idx), std::memory_order_relaxed); } + idx_byte_size_.fetch_sub(statistics_info->idx_byte_size - old.idx_byte_size); + DLOG(INFO) << "idx_byte_size_ " << idx_byte_size_ << " after sub " + << statistics_info->idx_byte_size - old.idx_byte_size; } void Segment::ExecuteGc(const TTLSt& ttl_st, StatisticsInfo* statistics_info) { @@ -434,11 +478,16 @@ void Segment::ExecuteGc(const TTLSt& ttl_st, StatisticsInfo* statistics_info) { } } -void Segment::ExecuteGc(const std::map& ttl_st_map, StatisticsInfo* statistics_info) { +void Segment::ExecuteGc(const std::map& ttl_st_map, StatisticsInfo* statistics_info, + std::optional clustered_ts_id) { if (ttl_st_map.empty()) { return; } if (ts_cnt_ <= 1) { + if (clustered_ts_id.has_value() && ts_idx_map_.begin()->first == clustered_ts_id.value()) { + DLOG(INFO) << "skip normal gc in cidx"; + return; + } ExecuteGc(ttl_st_map.begin()->second, statistics_info); return; } @@ -454,7 +503,7 @@ void Segment::ExecuteGc(const std::map& ttl_st_map, StatisticsI if (!need_gc) { return; } - GcAllType(ttl_st_map, statistics_info); + GcAllType(ttl_st_map, statistics_info, clustered_ts_id); } void Segment::Gc4Head(uint64_t keep_cnt, StatisticsInfo* statistics_info) { @@ -485,11 +534,16 @@ void Segment::Gc4Head(uint64_t keep_cnt, StatisticsInfo* statistics_info) { idx_cnt_vec_[0]->fetch_sub(statistics_info->GetIdxCnt(0) - old, std::memory_order_relaxed); } -void Segment::GcAllType(const std::map& ttl_st_map, StatisticsInfo* statistics_info) { +void Segment::GcAllType(const std::map& ttl_st_map, StatisticsInfo* statistics_info, + std::optional clustered_ts_id) { uint64_t old = statistics_info->GetTotalCnt(); uint64_t consumed = ::baidu::common::timer::get_micros(); std::unique_ptr it(entries_->NewIterator()); it->SeekToFirst(); + for (auto [ts, ttl_st] : ttl_st_map) { + DLOG(INFO) << "ts " << ts << " ttl_st " << ttl_st.ToString() << " it will be current time - ttl?"; + } + while (it->Valid()) { KeyEntry** entry_arr = reinterpret_cast(it->GetValue()); Slice key = it->GetKey(); @@ -501,6 +555,11 @@ void Segment::GcAllType(const std::map& ttl_st_map, StatisticsI } auto pos = ts_idx_map_.find(kv.first); if (pos == ts_idx_map_.end() || pos->second >= ts_cnt_) { + LOG(WARNING) << ""; + continue; + } + if (clustered_ts_id.has_value() && kv.first == clustered_ts_id.value()) { + DLOG(INFO) << "skip normal gc in cidx"; continue; } KeyEntry* entry = entry_arr[pos->second]; @@ -592,12 +651,13 @@ void Segment::GcAllType(const std::map& ttl_st_map, StatisticsI } } if (entry_node != nullptr) { + DLOG(INFO) << "add key " << key.ToString() << " to node cache. version " << gc_version_; node_cache_.AddKeyEntryNode(gc_version_.load(std::memory_order_relaxed), entry_node); } } } - DEBUGLOG("[GcAll] segment gc consumed %lu, count %lu", (::baidu::common::timer::get_micros() - consumed) / 1000, - statistics_info->GetTotalCnt() - old); + DLOG(INFO) << "[GcAll] segment gc consumed " << (::baidu::common::timer::get_micros() - consumed) / 1000 + << "ms, count " << statistics_info->GetTotalCnt() - old; } void Segment::SplitList(KeyEntry* entry, uint64_t ts, ::openmldb::base::Node** node) { @@ -745,6 +805,7 @@ void Segment::Gc4TTLOrHead(const uint64_t time, const uint64_t keep_cnt, Statist } } if (entry_node != nullptr) { + DLOG(INFO) << "add key " << key.ToString() << " to node cache. version " << gc_version_; node_cache_.AddKeyEntryNode(gc_version_.load(std::memory_order_relaxed), entry_node); } uint64_t cur_idx_cnt = statistics_info->GetIdxCnt(0); diff --git a/src/storage/segment.h b/src/storage/segment.h index 511df69e5c4..01a76374889 100644 --- a/src/storage/segment.h +++ b/src/storage/segment.h @@ -46,6 +46,7 @@ class MemTableIterator : public TableIterator { void Seek(const uint64_t time) override; bool Valid() override; void Next() override; + // GetXXX will core if it_==nullptr, don't use it without valid openmldb::base::Slice GetValue() const override; uint64_t GetKey() const override; void SeekToFirst() override; @@ -68,7 +69,7 @@ class Segment { public: explicit Segment(uint8_t height); Segment(uint8_t height, const std::vector& ts_idx_vec); - ~Segment(); + virtual ~Segment(); // legacy interface called by memtable and ut void Put(const Slice& key, uint64_t time, const char* data, uint32_t size, bool put_if_absent = false, @@ -78,22 +79,25 @@ class Segment { void BulkLoadPut(unsigned int key_entry_id, const Slice& key, uint64_t time, DataBlock* row); // main put method - bool Put(const Slice& key, const std::map& ts_map, DataBlock* row, bool put_if_absent = false); + virtual bool Put(const Slice& key, const std::map& ts_map, DataBlock* row, + bool put_if_absent = false); bool Delete(const std::optional& idx, const Slice& key); - bool Delete(const std::optional& idx, const Slice& key, - uint64_t ts, const std::optional& end_ts); + bool Delete(const std::optional& idx, const Slice& key, uint64_t ts, + const std::optional& end_ts); void Release(StatisticsInfo* statistics_info); void ExecuteGc(const TTLSt& ttl_st, StatisticsInfo* statistics_info); - void ExecuteGc(const std::map& ttl_st_map, StatisticsInfo* statistics_info); + void ExecuteGc(const std::map& ttl_st_map, StatisticsInfo* statistics_info, + std::optional clustered_ts_id = std::nullopt); void Gc4TTL(const uint64_t time, StatisticsInfo* statistics_info); void Gc4Head(uint64_t keep_cnt, StatisticsInfo* statistics_info); void Gc4TTLAndHead(const uint64_t time, const uint64_t keep_cnt, StatisticsInfo* statistics_info); void Gc4TTLOrHead(const uint64_t time, const uint64_t keep_cnt, StatisticsInfo* statistics_info); - void GcAllType(const std::map& ttl_st_map, StatisticsInfo* statistics_info); + void GcAllType(const std::map& ttl_st_map, StatisticsInfo* statistics_info, + std::optional clustered_ts_id = std::nullopt); MemTableIterator* NewIterator(const Slice& key, Ticket& ticket, type::CompressType compress_type); // NOLINT MemTableIterator* NewIterator(const Slice& key, uint32_t idx, Ticket& ticket, // NOLINT @@ -141,17 +145,17 @@ class Segment { void ReleaseAndCount(const std::vector& id_vec, StatisticsInfo* statistics_info); - private: + protected: void FreeList(uint32_t ts_idx, ::openmldb::base::Node* node, StatisticsInfo* statistics_info); void SplitList(KeyEntry* entry, uint64_t ts, ::openmldb::base::Node** node); bool GetTsIdx(const std::optional& idx, uint32_t* ts_idx); bool ListContains(KeyEntry* entry, uint64_t time, DataBlock* row, bool check_all_time); - bool PutUnlock(const Slice& key, uint64_t time, DataBlock* row, bool put_if_absent = false, - bool check_all_time = false); + virtual bool PutUnlock(const Slice& key, uint64_t time, DataBlock* row, bool put_if_absent = false, + bool check_all_time = false); - private: + protected: KeyEntries* entries_; std::mutex mu_; std::atomic idx_byte_size_; @@ -159,6 +163,7 @@ class Segment { uint8_t key_entry_max_height_; uint32_t ts_cnt_; std::atomic gc_version_; + // std::map ts_idx_map_; std::vector>> idx_cnt_vec_; uint64_t ttl_offset_; diff --git a/src/storage/table.cc b/src/storage/table.cc index 7126430a9d5..ebb27bf73ef 100644 --- a/src/storage/table.cc +++ b/src/storage/table.cc @@ -207,8 +207,10 @@ bool Table::AddIndex(const ::openmldb::common::ColumnKey& column_key) { } col_vec.push_back(it->second); } + + common::IndexType index_type = column_key.has_type() ? column_key.type() : common::IndexType::kCovering; index_def = std::make_shared(column_key.index_name(), table_index_.GetMaxIndexId() + 1, - IndexStatus::kReady, ::openmldb::type::IndexType::kTimeSerise, col_vec); + IndexStatus::kReady, ::openmldb::type::IndexType::kTimeSerise, col_vec, index_type); if (!column_key.ts_name().empty()) { if (auto ts_iter = schema.find(column_key.ts_name()); ts_iter == schema.end()) { PDLOG(WARNING, "not found ts_name[%s]. tid %u pid %u", column_key.ts_name().c_str(), id_, pid_); diff --git a/src/storage/table_iterator_test.cc b/src/storage/table_iterator_test.cc index 3af20940266..47847498e0d 100644 --- a/src/storage/table_iterator_test.cc +++ b/src/storage/table_iterator_test.cc @@ -152,7 +152,8 @@ TEST_P(TableIteratorTest, latest) { dim->set_key(key); std::string value; ASSERT_EQ(0, codec.EncodeRow(row, &value)); - table->Put(0, value, request.dimensions()); + auto st = table->Put(0, value, request.dimensions()); + ASSERT_TRUE(st.ok()) << st.ToString(); } } ::hybridse::vm::WindowIterator* it = table->NewWindowIterator(0); @@ -216,7 +217,8 @@ TEST_P(TableIteratorTest, smoketest2) { dim->set_key(key); std::string value; ASSERT_EQ(0, codec.EncodeRow(row, &value)); - table->Put(0, value, request.dimensions()); + auto st = table->Put(0, value, request.dimensions()); + ASSERT_TRUE(st.ok()) << st.ToString(); } } ::hybridse::vm::WindowIterator* it = table->NewWindowIterator(0); @@ -383,7 +385,8 @@ TEST_P(TableIteratorTest, releaseKeyIterator) { dim->set_key(key); std::string value; ASSERT_EQ(0, codec.EncodeRow(row, &value)); - table->Put(0, value, request.dimensions()); + auto st = table->Put(0, value, request.dimensions()); + ASSERT_TRUE(st.ok()) << st.ToString(); } } @@ -429,7 +432,8 @@ TEST_P(TableIteratorTest, SeekNonExistent) { dim->set_key(key); std::string value; ASSERT_EQ(0, codec.EncodeRow(row, &value)); - table->Put(0, value, request.dimensions()); + auto st = table->Put(0, value, request.dimensions()); + ASSERT_TRUE(st.ok()) << st.ToString(); } } diff --git a/src/tablet/tablet_impl.cc b/src/tablet/tablet_impl.cc index 230b5c46a09..45322ca03d5 100644 --- a/src/tablet/tablet_impl.cc +++ b/src/tablet/tablet_impl.cc @@ -18,6 +18,7 @@ #include #include + #include #include #include "vm/sql_compiler.h" @@ -35,8 +36,6 @@ #include "absl/cleanup/cleanup.h" #include "absl/time/clock.h" #include "absl/time/time.h" -#include "boost/bind.hpp" -#include "boost/container/deque.hpp" #include "base/file_util.h" #include "base/glog_wrapper.h" #include "base/hash.h" @@ -45,6 +44,8 @@ #include "base/status.h" #include "base/strings.h" #include "base/sys_info.h" +#include "boost/bind.hpp" +#include "boost/container/deque.hpp" #include "brpc/controller.h" #include "butil/iobuf.h" #include "codec/codec.h" @@ -63,6 +64,7 @@ #include "schema/schema_adapter.h" #include "storage/binlog.h" #include "storage/disk_table_snapshot.h" +#include "storage/index_organized_table.h" #include "storage/segment.h" #include "storage/table.h" #include "tablet/file_sender.h" @@ -202,7 +204,7 @@ bool TabletImpl::Init(const std::string& zk_cluster, const std::string& zk_path, if (!zk_cluster.empty()) { zk_client_ = new ZkClient(zk_cluster, real_endpoint, FLAGS_zk_session_timeout, endpoint, zk_path, - FLAGS_zk_auth_schema, FLAGS_zk_cert); + FLAGS_zk_auth_schema, FLAGS_zk_cert); bool ok = zk_client_->Init(); if (!ok) { PDLOG(ERROR, "fail to init zookeeper with cluster %s", zk_cluster.c_str()); @@ -375,8 +377,8 @@ void TabletImpl::UpdateTTL(RpcController* ctrl, const ::openmldb::api::UpdateTTL base::SetResponseStatus(base::ReturnCode::kWriteDataFailed, "write meta data failed", response); return; } - PDLOG(INFO, "update table tid %u pid %u ttl meta to abs_ttl %lu lat_ttl %lu index_name %s", tid, pid, abs_ttl, lat_ttl, - index_name.c_str()); + PDLOG(INFO, "update table tid %u pid %u ttl meta to abs_ttl %lu lat_ttl %lu index_name %s", tid, pid, abs_ttl, + lat_ttl, index_name.c_str()); response->set_code(::openmldb::base::ReturnCode::kOk); response->set_msg("ok"); } @@ -465,7 +467,7 @@ int32_t TabletImpl::GetIndex(const ::openmldb::api::GetRequest* request, const : const std::map>& vers_schema, CombineIterator* it, std::string* value, uint64_t* ts) { if (it == nullptr || value == nullptr || ts == nullptr) { - PDLOG(WARNING, "invalid args"); + LOG(WARNING) << "invalid args"; return -1; } uint64_t st = request->ts(); @@ -473,10 +475,12 @@ int32_t TabletImpl::GetIndex(const ::openmldb::api::GetRequest* request, const : uint64_t et = request->et(); const openmldb::api::GetType& et_type = request->et_type(); if (st_type == ::openmldb::api::kSubKeyEq && et_type == ::openmldb::api::kSubKeyEq && st != et) { + LOG(WARNING) << "invalid args for st " << st << " not equal to et " << et; return -1; } ::openmldb::api::GetType real_et_type = et_type; ::openmldb::storage::TTLType ttl_type = it->GetTTLType(); + uint64_t expire_time = it->GetExpireTime(); if (ttl_type == ::openmldb::storage::TTLType::kAbsoluteTime || ttl_type == ::openmldb::storage::TTLType::kAbsOrLat) { @@ -485,22 +489,28 @@ int32_t TabletImpl::GetIndex(const ::openmldb::api::GetRequest* request, const : if (et < expire_time && et_type == ::openmldb::api::GetType::kSubKeyGt) { real_et_type = ::openmldb::api::GetType::kSubKeyGe; } + DLOG(INFO) << "expire time " << expire_time << ", after adjust: et " << et << " real_et_type " << real_et_type; + bool enable_project = false; openmldb::codec::RowProject row_project(vers_schema, request->projection()); if (request->projection().size() > 0) { bool ok = row_project.Init(); if (!ok) { - PDLOG(WARNING, "invalid project list"); + LOG(WARNING) << "invalid project list"; return -1; } enable_project = true; } + // it's ok when st < et(after adjust), we should return 0 rows cuz no valid data for this range + // but we have set the code -1, don't change the return code, accept it. if (st > 0 && st < et) { - DEBUGLOG("invalid args for st %lu less than et %lu or expire time %lu", st, et, expire_time); + DLOG(WARNING) << "invalid args for st " << st << " less than et " << et; return -1; } + DLOG(INFO) << "it valid " << it->Valid(); if (it->Valid()) { *ts = it->GetTs(); + DLOG(INFO) << "check " << *ts << " " << st << " " << et << " " << st_type << " " << real_et_type; if (st_type == ::openmldb::api::GetType::kSubKeyEq && st > 0 && *ts != st) { return 1; } @@ -514,7 +524,7 @@ int32_t TabletImpl::GetIndex(const ::openmldb::api::GetRequest* request, const : const int8_t* row_ptr = reinterpret_cast(data.data()); bool ok = row_project.Project(row_ptr, data.size(), &ptr, &size); if (!ok) { - PDLOG(WARNING, "fail to make a projection"); + LOG(WARNING) << "fail to make a projection"; return -4; } value->assign(reinterpret_cast(ptr), size); @@ -544,7 +554,7 @@ int32_t TabletImpl::GetIndex(const ::openmldb::api::GetRequest* request, const : break; default: - PDLOG(WARNING, "invalid et type %s", ::openmldb::api::GetType_Name(et_type).c_str()); + LOG(WARNING) << "invalid et type " << ::openmldb::api::GetType_Name(et_type).c_str(); return -2; } if (jump_out) { @@ -557,7 +567,7 @@ int32_t TabletImpl::GetIndex(const ::openmldb::api::GetRequest* request, const : const int8_t* row_ptr = reinterpret_cast(data.data()); bool ok = row_project.Project(row_ptr, data.size(), &ptr, &size); if (!ok) { - PDLOG(WARNING, "fail to make a projection"); + LOG(WARNING) << "fail to make a projection"; return -4; } value->assign(reinterpret_cast(ptr), size); @@ -671,6 +681,7 @@ void TabletImpl::Get(RpcController* controller, const ::openmldb::api::GetReques int32_t code = GetIndex(request, *table_meta, vers_schema, &combine_it, value, &ts); response->set_ts(ts); response->set_code(code); + DLOG(WARNING) << "get key " << request->key() << " ts " << ts << " code " << code; uint64_t end_time = ::baidu::common::timer::get_micros(); if (start_time + FLAGS_query_slow_log_threshold < end_time) { std::string index_name; @@ -750,10 +761,43 @@ void TabletImpl::Put(RpcController* controller, const ::openmldb::api::PutReques response->set_msg("invalid dimension parameter"); return; } - DLOG(INFO) << "put data to tid " << tid << " pid " << pid << " with key " << request->dimensions(0).key(); - // 1. normal put: ok, invalid data - // 2. put if absent: ok, exists but ignore, invalid data - st = table->Put(entry.ts(), entry.value(), entry.dimensions(), request->put_if_absent()); + if (request->check_exists()) { + // table should be iot + auto iot = std::dynamic_pointer_cast(table); + if (!iot) { + response->set_code(::openmldb::base::ReturnCode::kTableMetaIsIllegal); + response->set_msg("table type is not iot"); + return; + } + DLOG(INFO) << "check data exists in tid " << tid << " pid " << pid << " with key " + << entry.dimensions(0).key() << " ts " << entry.ts(); + // ts is ts value when check exists + st = iot->CheckDataExists(entry.ts(), entry.dimensions()); + } else { + DLOG(INFO) << "put data to tid " << tid << " pid " << pid << " with key " << request->dimensions(0).key(); + // 1. normal put: ok, invalid data + // 2. put if absent: ok, exists but ignore, invalid data + st = table->Put(entry.ts(), entry.value(), entry.dimensions(), request->put_if_absent()); + } + } + // when check exists, we won't do log + if (request->check_exists()) { + DLOG_ASSERT(request->check_exists()) << "check_exists should be true"; + DLOG_ASSERT(!request->put_if_absent()) << "put_if_absent should be false"; + DLOG(INFO) << "result " << st.ToString(); + // return ok if exists + if (absl::IsAlreadyExists(st)) { + response->set_code(base::ReturnCode::kOk); + response->set_msg("exists"); + } else if (absl::IsNotFound(st)) { + response->set_code(base::ReturnCode::kKeyNotFound); + response->set_msg(st.ToString()); + } else { + // other errors + response->set_code(base::ReturnCode::kError); + response->set_msg(st.ToString()); + } + return; } if (!st.ok()) { @@ -1333,7 +1377,7 @@ void TabletImpl::Traverse(RpcController* controller, const ::openmldb::api::Trav } base::Status TabletImpl::CheckTable(uint32_t tid, uint32_t pid, bool check_leader, - const std::shared_ptr& table) { + const std::shared_ptr
& table) { if (!table) { PDLOG(WARNING, "table does not exist. tid %u, pid %u", tid, pid); return {base::ReturnCode::kTableIsNotExist, "table does not exist"}; @@ -1350,15 +1394,16 @@ base::Status TabletImpl::CheckTable(uint32_t tid, uint32_t pid, bool check_leade } base::Status TabletImpl::DeleteAllIndex(const std::shared_ptr& table, - const std::shared_ptr& cur_index, - const std::string& key, - std::optional start_ts, - std::optional end_ts, + const std::shared_ptr& cur_index, const std::string& key, + std::optional start_ts, std::optional end_ts, bool skip_cur_ts_col, const std::shared_ptr& client_manager, uint32_t partition_num) { storage::Ticket ticket; std::unique_ptr iter(table->NewIterator(cur_index->GetId(), key, ticket)); + DLOG(INFO) << "delete all index in " << table->GetId() << "." << cur_index->GetId() << ", key " << key + << ", start_ts " << (start_ts.has_value() ? std::to_string(start_ts.value()) : "-1") << ", end_ts " + << (end_ts.has_value() ? std::to_string(end_ts.value()) : "-1"); if (start_ts.has_value()) { iter->Seek(start_ts.value()); } else { @@ -1366,7 +1411,7 @@ base::Status TabletImpl::DeleteAllIndex(const std::shared_ptr& t } auto indexs = table->GetAllIndex(); while (iter->Valid()) { - DEBUGLOG("cur ts %lu cur index pos %u", iter->GetKey(), cur_index->GetId()); + DLOG(INFO) << "cur ts " << iter->GetKey(); if (end_ts.has_value() && iter->GetKey() <= end_ts.value()) { break; } @@ -1450,14 +1495,13 @@ base::Status TabletImpl::DeleteAllIndex(const std::shared_ptr& t if (client == nullptr) { return {base::ReturnCode::kDeleteFailed, absl::StrCat("client is nullptr, pid ", cur_pid)}; } - DEBUGLOG("delete idx %u pid %u pk %s ts %lu end_ts %lu", - option.idx.value(), cur_pid, option.key.c_str(), option.start_ts.value(), option.end_ts.value()); std::string msg; // do not delete other index data option.enable_decode_value = false; + DLOG(INFO) << "pid " << cur_pid << " delete key " << option.DebugString(); if (auto status = client->Delete(table->GetId(), cur_pid, option, FLAGS_request_timeout_ms); !status.OK()) { return {base::ReturnCode::kDeleteFailed, - absl::StrCat("delete failed. key ", option.key, " pid ", cur_pid, " msg: ", status.GetMsg())}; + absl::StrCat("delete failed. key ", option.key, " pid ", cur_pid, " msg: ", status.GetMsg())}; } } @@ -1478,7 +1522,7 @@ void TabletImpl::Delete(RpcController* controller, const ::openmldb::api::Delete } auto table = GetTable(tid, pid); if (auto status = CheckTable(tid, pid, true, table); !status.OK()) { - SetResponseStatus(status, response); + SET_RESP_AND_WARN(response, status.GetCode(), status.GetMsg()); return; } auto replicator = GetReplicator(tid, pid); @@ -1547,13 +1591,14 @@ void TabletImpl::Delete(RpcController* controller, const ::openmldb::api::Delete } } } + DLOG(INFO) << tid << "." << pid << ": delete request " << request->ShortDebugString() << ", delete others " + << delete_others; auto aggrs = GetAggregators(tid, pid); if (!aggrs && !delete_others) { if (table->Delete(entry)) { - DEBUGLOG("delete ok. tid %u, pid %u, key %s", tid, pid, request->key().c_str()); + DLOG(INFO) << tid << "." << pid << ": delete ok, key " << request->key(); } else { - response->set_code(::openmldb::base::ReturnCode::kDeleteFailed); - response->set_msg("delete failed"); + SET_RESP_AND_WARN(response, base::ReturnCode::kDeleteFailed, "delete failed"); return; } } else { @@ -1585,36 +1630,37 @@ void TabletImpl::Delete(RpcController* controller, const ::openmldb::api::Delete } uint32_t pid_num = tablet_table_handler->GetPartitionNum(); auto table_client_manager = tablet_table_handler->GetTableClientManager(); + DLOG(INFO) << "delete from table & aggr " << tid << "." << pid; if (entry.dimensions_size() > 0) { const auto& dimension = entry.dimensions(0); uint32_t idx = dimension.idx(); auto index_def = table->GetIndex(idx); const auto& key = dimension.key(); if (delete_others) { - auto status = DeleteAllIndex(table, index_def, key, start_ts, end_ts, false, - table_client_manager, pid_num); + auto status = + DeleteAllIndex(table, index_def, key, start_ts, end_ts, false, table_client_manager, pid_num); if (!status.OK()) { SET_RESP_AND_WARN(response, status.GetCode(), status.GetMsg()); return; } } if (!table->Delete(idx, key, start_ts, end_ts)) { - response->set_code(::openmldb::base::ReturnCode::kDeleteFailed); - response->set_msg("delete failed"); + SET_RESP_AND_WARN(response, base::ReturnCode::kDeleteFailed, "delete from partition failed"); return; } auto aggr = get_aggregator(aggrs, idx); if (aggr) { if (!aggr->Delete(key, start_ts, end_ts)) { - PDLOG(WARNING, "delete from aggr failed. base table: tid[%u] pid[%u] index[%u] key[%s]. " - "aggr table: tid[%u]", + PDLOG(WARNING, + "delete from aggr failed. base table: tid[%u] pid[%u] index[%u] key[%s]. " + "aggr table: tid[%u]", tid, pid, idx, key.c_str(), aggr->GetAggrTid()); response->set_code(::openmldb::base::ReturnCode::kDeleteFailed); response->set_msg("delete from associated pre-aggr table failed"); return; } } - DEBUGLOG("delete ok. tid %u, pid %u, key %s", tid, pid, key.c_str()); + DLOG(INFO) << tid << "." << pid << ": table & agg delete ok, key " << key; } else { bool is_first_hit_index = true; for (const auto& index_def : table->GetAllIndex()) { @@ -1630,8 +1676,8 @@ void TabletImpl::Delete(RpcController* controller, const ::openmldb::api::Delete while (iter->Valid()) { auto pk = iter->GetPK(); if (delete_others && is_first_hit_index) { - auto status = DeleteAllIndex(table, index_def, pk, start_ts, end_ts, true, - table_client_manager, pid_num); + auto status = + DeleteAllIndex(table, index_def, pk, start_ts, end_ts, true, table_client_manager, pid_num); if (!status.OK()) { SET_RESP_AND_WARN(response, status.GetCode(), status.GetMsg()); return; @@ -1639,15 +1685,16 @@ void TabletImpl::Delete(RpcController* controller, const ::openmldb::api::Delete } iter->NextPK(); if (!table->Delete(idx, pk, start_ts, end_ts)) { - response->set_code(::openmldb::base::ReturnCode::kDeleteFailed); - response->set_msg("delete failed"); + SET_RESP_AND_WARN(response, base::ReturnCode::kDeleteFailed, "delete failed"); return; } auto aggr = get_aggregator(aggrs, idx); if (aggr) { if (!aggr->Delete(pk, start_ts, end_ts)) { - PDLOG(WARNING, "delete from aggr failed. base table: tid[%u] pid[%u] index[%u] key[%s]. " - "aggr table: tid[%u]", tid, pid, idx, pk.c_str(), aggr->GetAggrTid()); + PDLOG(WARNING, + "delete from aggr failed. base table: tid[%u] pid[%u] index[%u] key[%s]. " + "aggr table: tid[%u]", + tid, pid, idx, pk.c_str(), aggr->GetAggrTid()); response->set_code(::openmldb::base::ReturnCode::kDeleteFailed); response->set_msg("delete from associated pre-aggr table failed"); return; @@ -1656,11 +1703,11 @@ void TabletImpl::Delete(RpcController* controller, const ::openmldb::api::Delete } is_first_hit_index = false; } + DLOG(INFO) << tid << "." << pid << ": table & agg delete ok when no entry dim."; } } response->set_code(::openmldb::base::ReturnCode::kOk); response->set_msg("ok"); - replicator->AppendEntry(entry); if (FLAGS_binlog_notify_on_put) { replicator->Notify(); @@ -2563,7 +2610,7 @@ void TabletImpl::SetExpire(RpcController* controller, const ::openmldb::api::Set } void TabletImpl::MakeSnapshotInternal(uint32_t tid, uint32_t pid, uint64_t end_offset, - std::shared_ptr<::openmldb::api::TaskInfo> task, bool is_force) { + std::shared_ptr<::openmldb::api::TaskInfo> task, bool is_force) { PDLOG(INFO, "MakeSnapshotInternal begin, tid[%u] pid[%u]", tid, pid); std::shared_ptr
table; std::shared_ptr snapshot; @@ -3115,8 +3162,8 @@ void TabletImpl::LoadTable(RpcController* controller, const ::openmldb::api::Loa std::string db_path = GetDBPath(root_path, tid, pid); if (!::openmldb::base::IsExists(db_path)) { - PDLOG(WARNING, "table db path does not exist, but still load. tid %u, pid %u, path %s", - tid, pid, db_path.c_str()); + PDLOG(WARNING, "table db path does not exist, but still load. tid %u, pid %u, path %s", tid, pid, + db_path.c_str()); } std::shared_ptr
table = GetTable(tid, pid); @@ -3539,7 +3586,7 @@ void TabletImpl::CreateTable(RpcController* controller, const ::openmldb::api::C } void TabletImpl::TruncateTable(RpcController* controller, const ::openmldb::api::TruncateTableRequest* request, - ::openmldb::api::TruncateTableResponse* response, Closure* done) { + ::openmldb::api::TruncateTableResponse* response, Closure* done) { brpc::ClosureGuard done_guard(done); uint32_t tid = request->tid(); uint32_t pid = request->pid(); @@ -3552,8 +3599,8 @@ void TabletImpl::TruncateTable(RpcController* controller, const ::openmldb::api: for (const auto& aggr : *aggrs) { auto agg_table = aggr->GetAggTable(); if (!agg_table) { - PDLOG(WARNING, "aggrate table does not exist. tid[%u] pid[%u] index pos[%u]", - tid, pid, aggr->GetIndexPos()); + PDLOG(WARNING, "aggrate table does not exist. tid[%u] pid[%u] index pos[%u]", tid, pid, + aggr->GetIndexPos()); response->set_code(::openmldb::base::ReturnCode::kTableIsNotExist); response->set_msg("aggrate table does not exist"); return; @@ -3561,13 +3608,13 @@ void TabletImpl::TruncateTable(RpcController* controller, const ::openmldb::api: uint32_t agg_tid = agg_table->GetId(); uint32_t agg_pid = agg_table->GetPid(); if (auto status = TruncateTableInternal(agg_tid, agg_pid); !status.OK()) { - PDLOG(WARNING, "truncate aggrate table failed. tid[%u] pid[%u] index pos[%u]", - agg_tid, agg_pid, aggr->GetIndexPos()); + PDLOG(WARNING, "truncate aggrate table failed. tid[%u] pid[%u] index pos[%u]", agg_tid, agg_pid, + aggr->GetIndexPos()); base::SetResponseStatus(status, response); return; } - PDLOG(INFO, "truncate aggrate table success. tid[%u] pid[%u] index pos[%u]", - agg_tid, agg_pid, aggr->GetIndexPos()); + PDLOG(INFO, "truncate aggrate table success. tid[%u] pid[%u] index pos[%u]", agg_tid, agg_pid, + aggr->GetIndexPos()); } } response->set_code(::openmldb::base::ReturnCode::kOk); @@ -3620,8 +3667,8 @@ base::Status TabletImpl::TruncateTableInternal(uint32_t tid, uint32_t pid) { if (catalog_->AddTable(*table_meta, new_table)) { LOG(INFO) << "add table " << table_meta->name() << " to catalog with db " << table_meta->db(); } else { - LOG(WARNING) << "fail to add table " << table_meta->name() - << " to catalog with db " << table_meta->db(); + LOG(WARNING) << "fail to add table " << table_meta->name() << " to catalog with db " + << table_meta->db(); return {::openmldb::base::ReturnCode::kCatalogUpdateFailed, "fail to update catalog"}; } } @@ -3659,7 +3706,7 @@ void TabletImpl::ExecuteGc(RpcController* controller, const ::openmldb::api::Exe gc_pool_.AddTask(boost::bind(&TabletImpl::GcTable, this, tid, pid, true)); response->set_code(::openmldb::base::ReturnCode::kOk); response->set_msg("ok"); - PDLOG(INFO, "ExecuteGc. tid %u pid %u", tid, pid); + PDLOG(INFO, "ExecuteGc add task. tid %u pid %u", tid, pid); } void TabletImpl::GetTableFollower(RpcController* controller, const ::openmldb::api::GetTableFollowerRequest* request, @@ -4025,6 +4072,25 @@ int TabletImpl::UpdateTableMeta(const std::string& path, ::openmldb::api::TableM return UpdateTableMeta(path, table_meta, false); } +bool IsIOT(const ::openmldb::api::TableMeta* table_meta) { + auto cks = table_meta->column_key(); + if (cks.empty()) { + LOG(WARNING) << "no index in meta"; + return false; + } + if (cks[0].has_type() && cks[0].type() == common::IndexType::kClustered) { + // check other indexes + for (int i = 1; i < cks.size(); i++) { + if (cks[i].has_type() && cks[i].type() == common::IndexType::kClustered) { + LOG(WARNING) << "should be only one clustered index"; + return false; + } + } + return true; + } + return false; +} + int TabletImpl::CreateTableInternal(const ::openmldb::api::TableMeta* table_meta, std::string& msg) { uint32_t tid = table_meta->tid(); uint32_t pid = table_meta->pid(); @@ -4054,7 +4120,12 @@ int TabletImpl::CreateTableInternal(const ::openmldb::api::TableMeta* table_meta } std::string table_db_path = GetDBPath(db_root_path, tid, pid); if (table_meta->storage_mode() == openmldb::common::kMemory) { - table = std::make_shared(*table_meta); + if (IsIOT(table_meta)) { + LOG(INFO) << "create iot table " << tid << "." << pid; + table = std::make_shared(*table_meta, catalog_); + } else { + table = std::make_shared(*table_meta); + } } else { table = std::make_shared(*table_meta, table_db_path); } @@ -4292,7 +4363,16 @@ void TabletImpl::GcTable(uint32_t tid, uint32_t pid, bool execute_once) { std::shared_ptr
table = GetTable(tid, pid); if (table) { int32_t gc_interval = table->GetStorageMode() == common::kMemory ? FLAGS_gc_interval : FLAGS_disk_gc_interval; - table->SchedGc(); + if (auto iot = std::dynamic_pointer_cast(table); iot) { + sdk::SQLRouterOptions options; + options.zk_cluster = zk_cluster_; + options.zk_path = zk_path_; + auto router = sdk::NewClusterSQLRouter(options); + iot->SchedGCByDelete(router); // add a lock to avoid gc one table in the same time + } else { + table->SchedGc(); + } + if (!execute_once) { gc_pool_.DelayTask(gc_interval * 60 * 1000, boost::bind(&TabletImpl::GcTable, this, tid, pid, false)); } @@ -5228,12 +5308,12 @@ void TabletImpl::ExtractIndexData(RpcController* controller, const ::openmldb::a index_vec.push_back(cur_column_key); } if (IsClusterMode()) { - task_pool_.AddTask(boost::bind(&TabletImpl::ExtractIndexDataInternal, this, table, snapshot, - index_vec, request->partition_num(), request->offset(), request->dump_data(), + task_pool_.AddTask(boost::bind(&TabletImpl::ExtractIndexDataInternal, this, table, snapshot, index_vec, + request->partition_num(), request->offset(), request->dump_data(), task_ptr)); } else { - ExtractIndexDataInternal(table, snapshot, index_vec, request->partition_num(), request->offset(), - false, nullptr); + ExtractIndexDataInternal(table, snapshot, index_vec, request->partition_num(), request->offset(), false, + nullptr); } base::SetResponseOK(response); return; @@ -5850,9 +5930,10 @@ bool TabletImpl::CreateAggregatorInternal(const ::openmldb::api::CreateAggregato PDLOG(WARNING, "base table does not exist. tid %u, pid %u", base_meta.tid(), base_meta.pid()); return false; } - auto aggregator = ::openmldb::storage::CreateAggregator(base_meta, base_table, - *aggr_table->GetTableMeta(), aggr_table, aggr_replicator, request->index_pos(), request->aggr_col(), - request->aggr_func(), request->order_by_col(), request->bucket_size(), request->filter_col()); + auto aggregator = ::openmldb::storage::CreateAggregator( + base_meta, base_table, *aggr_table->GetTableMeta(), aggr_table, aggr_replicator, request->index_pos(), + request->aggr_col(), request->aggr_func(), request->order_by_col(), request->bucket_size(), + request->filter_col()); if (!aggregator) { msg.assign("create aggregator failed"); return false; @@ -5925,10 +6006,11 @@ TabletImpl::GetSystemTableIterator() { } auto schema = std::make_unique<::openmldb::codec::Schema>(); - + if (openmldb::schema::SchemaAdapter::ConvertSchema(*tablet_table_handler->GetSchema(), schema.get())) { std::map> tablet_clients = {{0, client}}; - return {{std::make_unique(tablet_table_handler->GetTid(), nullptr, tablet_clients), + return { + {std::make_unique(tablet_table_handler->GetTid(), nullptr, tablet_clients), std::move(schema)}}; } else { return std::nullopt; diff --git a/src/tablet/tablet_impl_test.cc b/src/tablet/tablet_impl_test.cc index 985c59e51d3..3df6a8e3553 100644 --- a/src/tablet/tablet_impl_test.cc +++ b/src/tablet/tablet_impl_test.cc @@ -6249,7 +6249,8 @@ TEST_F(TabletImplTest, DeleteRange) { ::openmldb::common::ColumnDesc* column_desc2 = table_meta->add_column_desc(); column_desc2->set_name("mcc"); column_desc2->set_data_type(::openmldb::type::kString); - SchemaCodec::SetIndex(table_meta->add_column_key(), "card", "card", "", ::openmldb::type::kAbsoluteTime, 120, 0); + // insert time ttl and 120 min, so data won't be gc by ttl + SchemaCodec::SetIndex(table_meta->add_column_key(), "card_idx", "card", "", ::openmldb::type::kAbsoluteTime, 120, 0); ::openmldb::api::CreateTableResponse response; tablet.CreateTable(NULL, &request, &response, &closure); @@ -6293,16 +6294,19 @@ TEST_F(TabletImplTest, DeleteRange) { delete_request.set_pid(1); delete_request.set_end_ts(1); tablet.Delete(NULL, &delete_request, &gen_response, &closure); - ASSERT_EQ(0, gen_response.code()); + ASSERT_EQ(0, gen_response.code()) << gen_response.ShortDebugString(); ::openmldb::api::ExecuteGcRequest e_request; e_request.set_tid(id); e_request.set_pid(1); + // async task, need to wait + // segment: entries -> node cache tablet.ExecuteGc(NULL, &e_request, &gen_response, &closure); + ASSERT_EQ(0, gen_response.code()) << gen_response.ShortDebugString(); sleep(2); + assert_status(100, 3400, 5786); // before node cache gc, status will be the same + // gc node cache tablet.ExecuteGc(NULL, &e_request, &gen_response, &closure); - sleep(2); - assert_status(0, 0, 1626); - tablet.ExecuteGc(NULL, &e_request, &gen_response, &closure); + ASSERT_EQ(0, gen_response.code()) << gen_response.ShortDebugString(); sleep(2); assert_status(0, 0, 0); } diff --git a/steps/test_python.sh b/steps/test_python.sh index 8c366f77b0c..3e3588b0db7 100644 --- a/steps/test_python.sh +++ b/steps/test_python.sh @@ -42,8 +42,8 @@ python3 -m pip install "${whl_name_sdk}[test]" cd "${ROOT_DIR}"/python/openmldb_tool/dist/ whl_name_tool=$(ls openmldb*.whl) echo "whl_name_tool:${whl_name_tool}" -# pip 23.1.2 just needs to install test(rpc is required by test) -python3 -m pip install "${whl_name_tool}[rpc,test]" +# pip 23.1.2 just needs to install test +python3 -m pip install "${whl_name_tool}[test]" python3 -m pip install pytest-cov diff --git a/tools/tool.py b/tools/tool.py index b95a6246fc5..4f92f2a4098 100644 --- a/tools/tool.py +++ b/tools/tool.py @@ -219,7 +219,7 @@ def GetTableInfoHTTP(self, database, table_name = ''): ns = self.endpoint_map[self.ns_leader] conn = httplib.HTTPConnection(ns) param = {"db": database, "name": table_name} - headers = {"Content-type": "application/json"} + headers = {"Content-type": "application/json", "Authorization": "foo"} conn.request("POST", "/NameServer/ShowTable", json.dumps(param), headers) response = conn.getresponse() if response.status != 200: @@ -233,13 +233,15 @@ def GetTableInfoHTTP(self, database, table_name = ''): def ParseTableInfo(self, table_info): result = {} + if not table_info: + return Status(-1, "table info is empty"), None for record in table_info: is_leader = True if record[4] == "leader" else False is_alive = True if record[5] == "yes" else False partition = Partition(record[0], record[1], record[2], record[3], is_leader, is_alive, record[6]) result.setdefault(record[2], []) result[record[2]].append(partition) - return result + return Status(), result def ParseTableInfoJson(self, table_info): """parse one table's partition info from json""" @@ -260,8 +262,7 @@ def GetTablePartition(self, database, table_name): status, result = self.GetTableInfo(database, table_name) if not status.OK: return status, None - partition_dict = self.ParseTableInfo(result) - return Status(), partition_dict + return self.ParseTableInfo(result) def GetAllTable(self, database): status, result = self.GetTableInfo(database) @@ -323,7 +324,7 @@ def LoadTableHTTP(self, endpoint, name, tid, pid, storage): # ttl won't effect, set to 0, and seg cnt is always 8 # and no matter if leader param = {"table_meta": {"name": name, "tid": tid, "pid": pid, "ttl":0, "seg_cnt":8, "storage_mode": storage}} - headers = {"Content-type": "application/json"} + headers = {"Content-type": "application/json", "Authorization": "foo"} conn.request("POST", "/TabletServer/LoadTable", json.dumps(param), headers) response = conn.getresponse() if response.status != 200: From b7e592c2a46cbb480e3233d97c47b66a66636102 Mon Sep 17 00:00:00 2001 From: yangwucheng Date: Mon, 1 Jul 2024 21:50:10 +0800 Subject: [PATCH 26/41] feat(open-mysql-db): pandas support (#3868) * feat(open-mysql-db): refactor 1. remove unnecessary instance var port 2. fix cause null bug 3. remove unnecessary throws 4. fix ctx.close() sequence bug 5. config sessionTimeout and requestTimeout 6. add docs of SqlEngine * feat(open-mysql-db): refactor * feat(open-mysql-db): revert passsword * feat(open-mysql-db): mock commit and schema table count * feat(open-mysql-db): replace data type text with string * feat(open-mysql-db): remove null --------- Co-authored-by: yangwucheng --- .../open-mysql-db/python-testcases/main.py | 33 ++++++++++ .../java/cn/paxos/mysql/MySqlListener.java | 3 + .../mysql/server/OpenmldbMysqlServer.java | 61 ++++++++++++++++++- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 extensions/open-mysql-db/python-testcases/main.py diff --git a/extensions/open-mysql-db/python-testcases/main.py b/extensions/open-mysql-db/python-testcases/main.py new file mode 100644 index 00000000000..a673e63def3 --- /dev/null +++ b/extensions/open-mysql-db/python-testcases/main.py @@ -0,0 +1,33 @@ +import pandas as pd +from sqlalchemy import create_engine + +if __name__ == '__main__': + # Create a Pandas DataFrame (replace this with your actual data) + data = {'id': [1, 2, 3], + 'name': ['Alice', 'Bob', 'Charlie'], + 'age': [25, 30, 35], + 'score': [1.1, 2.2, 3.3], + 'ts': [pd.Timestamp.utcnow().timestamp(), pd.Timestamp.utcnow().timestamp(), + pd.Timestamp.utcnow().timestamp()], + 'dt': [pd.to_datetime('20240101', format='%Y%m%d'), pd.to_datetime('20240201', format='%Y%m%d'), + pd.to_datetime('20240301', format='%Y%m%d')], + } + df = pd.DataFrame(data) + + # Create a MySQL database engine using SQLAlchemy + engine = create_engine('mysql+pymysql://root:root@127.0.0.1:3307/demo_db') + + # Replace 'username', 'password', 'host', and 'db_name' with your actual database credentials + + # Define the name of the table in the database where you want to write the data + table_name = 'demo_table1' + + # Write the DataFrame 'df' into the MySQL table + df.to_sql(table_name, engine, if_exists='replace', index=False) + + # 'if_exists' parameter options: + # - 'fail': If the table already exists, an error will be raised. + # - 'replace': If the table already exists, it will be replaced. + # - 'append': If the table already exists, data will be appended to it. + + print("Data written to MySQL table successfully!") diff --git a/extensions/open-mysql-db/src/main/java/cn/paxos/mysql/MySqlListener.java b/extensions/open-mysql-db/src/main/java/cn/paxos/mysql/MySqlListener.java index a775a3ac78f..e9dc36d4715 100644 --- a/extensions/open-mysql-db/src/main/java/cn/paxos/mysql/MySqlListener.java +++ b/extensions/open-mysql-db/src/main/java/cn/paxos/mysql/MySqlListener.java @@ -217,6 +217,9 @@ private void handleQuery( && !queryStringWithoutComment.startsWith("set @@execute_mode=")) { // ignore SET command ctx.writeAndFlush(OkResponse.builder().sequenceId(query.getSequenceId() + 1).build()); + } else if (queryStringWithoutComment.equalsIgnoreCase("COMMIT")) { + // ignore COMMIT command + ctx.writeAndFlush(OkResponse.builder().sequenceId(query.getSequenceId() + 1).build()); } else if (useDbMatcher.matches()) { sqlEngine.useDatabase(getConnectionId(ctx), useDbMatcher.group(1)); ctx.writeAndFlush(OkResponse.builder().sequenceId(query.getSequenceId() + 1).build()); diff --git a/extensions/open-mysql-db/src/main/java/com/_4paradigm/openmldb/mysql/server/OpenmldbMysqlServer.java b/extensions/open-mysql-db/src/main/java/com/_4paradigm/openmldb/mysql/server/OpenmldbMysqlServer.java index 0e9a0ce8ec3..b5af68c7e75 100644 --- a/extensions/open-mysql-db/src/main/java/com/_4paradigm/openmldb/mysql/server/OpenmldbMysqlServer.java +++ b/extensions/open-mysql-db/src/main/java/com/_4paradigm/openmldb/mysql/server/OpenmldbMysqlServer.java @@ -10,6 +10,7 @@ import com._4paradigm.openmldb.jdbc.SQLResultSet; import com._4paradigm.openmldb.mysql.mock.MockResult; import com._4paradigm.openmldb.mysql.util.TypeUtil; +import com._4paradigm.openmldb.proto.NS; import com._4paradigm.openmldb.sdk.Column; import com._4paradigm.openmldb.sdk.Schema; import com._4paradigm.openmldb.sdk.SdkOption; @@ -56,6 +57,12 @@ public class OpenmldbMysqlServer { Pattern.compile( "(?i)SELECT COUNT\\(\\*\\) FROM information_schema\\.TABLES WHERE TABLE_SCHEMA = '(.+)'"); + // SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'xzs' AND table_name = + // 't_exam_paper' + private final Pattern selectCountSchemaTablesPattern = + Pattern.compile( + "(?i)SELECT COUNT\\(\\*\\) FROM information_schema\\.TABLES WHERE TABLE_SCHEMA = '(.+)' AND table_name = '(.+)'"); + // SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'xzs' private final Pattern selectCountColumnsPattern = Pattern.compile( @@ -182,6 +189,10 @@ public void query( return; } + if (mockSelectSchemaTableCount(connectionId, resultSetWriter, sql)) { + return; + } + // This mock must execute before mockPatternQuery // SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'demo_db' // UNION SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = @@ -240,6 +251,7 @@ public void query( return; } + String originalSql = sql; if (sql.startsWith("SHOW FULL TABLES")) { // SHOW FULL TABLES WHERE Table_type != 'VIEW' Matcher showTablesFromDbMatcher = showTablesFromDbPattern.matcher(sql); @@ -250,6 +262,14 @@ public void query( } else { sql = "SHOW TABLES"; } + } else if (sql.matches("(?i)(?s)^\\s*CREATE TABLE.*$")) { + // convert data type TEXT to STRING + sql = sql.replaceAll("(?i) TEXT", " STRING"); + // sql = sql.replaceAll("(?i) DATETIME", " DATE"); + if (!sql.toLowerCase().contains(" not null") + && sql.toLowerCase().contains(" null")) { + sql = sql.replaceAll("(?i) null", ""); + } } else { Matcher crateDatabaseMatcher = createDatabasePattern.matcher(sql); Matcher selectLimitMatcher = selectLimitPattern.matcher(sql); @@ -264,7 +284,7 @@ public void query( if (sql.toLowerCase().startsWith("select") || sql.toLowerCase().startsWith("show")) { SQLResultSet resultSet = (SQLResultSet) stmt.getResultSet(); - outputResultSet(resultSetWriter, resultSet, sql); + outputResultSet(resultSetWriter, resultSet, originalSql); } System.out.println("Success to execute OpenMLDB SQL: " + sql); @@ -622,6 +642,36 @@ private boolean mockPatternQuery(ResultSetWriter resultSetWriter, String sql) { return false; } + private boolean mockSelectSchemaTableCount( + int connectionId, ResultSetWriter resultSetWriter, String sql) throws SQLException { + // SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'xzs' AND + // table_name = 't_exam_paper' + Matcher selectCountSchemaTablesMatcher = selectCountSchemaTablesPattern.matcher(sql); + if (selectCountSchemaTablesMatcher.matches()) { + // COUNT(*) + List columns = new ArrayList<>(); + columns.add(new QueryResultColumn("COUNT(*)", "VARCHAR(255)")); + resultSetWriter.writeColumns(columns); + + List row; + String dbName = selectCountSchemaTablesMatcher.group(1); + String tableName = selectCountSchemaTablesMatcher.group(2); + row = new ArrayList<>(); + NS.TableInfo tableInfo = + sqlClusterExecutorMap.get(connectionId).getTableInfo(dbName, tableName); + if (tableInfo == null || tableInfo.getName().equals("")) { + row.add("0"); + } else { + row.add("1"); + } + resultSetWriter.writeRow(row); + + resultSetWriter.finish(); + return true; + } + return false; + } + private boolean mockSelectCountUnion( int connectionId, ResultSetWriter resultSetWriter, String sql) throws SQLException { // SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'demo_db' @@ -713,7 +763,8 @@ public void outputResultSet(ResultSetWriter resultSetWriter, SQLResultSet result // Add schema for (int i = 0; i < columnCount; i++) { String columnName = schema.getColumnName(i); - if (sql.equalsIgnoreCase("show table status") && columnName.equalsIgnoreCase("table_id")) { + if ((sql.startsWith("SHOW FULL TABLES") || sql.equalsIgnoreCase("show table status")) + && columnName.equalsIgnoreCase("table_id")) { tableIdColumnIndex = i; continue; } @@ -721,6 +772,9 @@ public void outputResultSet(ResultSetWriter resultSetWriter, SQLResultSet result columns.add( new QueryResultColumn(columnName, TypeUtil.openmldbTypeToMysqlTypeString(columnType))); } + if (sql.startsWith("SHOW FULL TABLES")) { + columns.add(new QueryResultColumn("Table_type", "VARCHAR(255)")); + } resultSetWriter.writeColumns(columns); @@ -739,6 +793,9 @@ public void outputResultSet(ResultSetWriter resultSetWriter, SQLResultSet result String columnValue = TypeUtil.getResultSetStringColumn(resultSet, i + 1, type); row.add(columnValue); } + if (sql.startsWith("SHOW FULL TABLES")) { + row.add("BASE TABLE"); + } resultSetWriter.writeRow(row); } From c8ae8f8c838f7e388df48b3df074b2db0cfaeeff Mon Sep 17 00:00:00 2001 From: HuangWei Date: Tue, 2 Jul 2024 11:03:57 +0800 Subject: [PATCH 27/41] fix: drop aggr tables in drop table (#3908) * fix: drop aggr tables in drop table * fix * fix test * fix * fix --------- Co-authored-by: Huang Wei --- src/cmd/sql_cmd_test.cc | 198 ++++++++-------------------------- src/sdk/sql_cluster_router.cc | 77 ++++++------- 2 files changed, 77 insertions(+), 198 deletions(-) diff --git a/src/cmd/sql_cmd_test.cc b/src/cmd/sql_cmd_test.cc index 0b29ae449cd..63acf9f70bf 100644 --- a/src/cmd/sql_cmd_test.cc +++ b/src/cmd/sql_cmd_test.cc @@ -1628,19 +1628,18 @@ TEST_P(DBSDKTest, DeployLongWindows) { rs = sr->ExecuteSQL("", result_sql, &status); ASSERT_EQ(3, rs->Size()); - std::string msg; - auto ok = sr->ExecuteDDL(openmldb::nameserver::PRE_AGG_DB, "drop table pre_test2_demo1_w1_sum_c4;", &status); - ASSERT_TRUE(ok); - ok = sr->ExecuteDDL(openmldb::nameserver::PRE_AGG_DB, "drop table pre_test2_demo1_w2_max_c5;", &status); - ASSERT_TRUE(ok); - ok = sr->ExecuteDDL(openmldb::nameserver::PRE_AGG_DB, "drop table pre_test2_demo3_w1_count_where_c4_c3;", &status); - ASSERT_TRUE(ok); - ASSERT_FALSE(cs->GetNsClient()->DropTable("test2", "trans", msg)); - ASSERT_TRUE(cs->GetNsClient()->DropProcedure("test2", "demo1", msg)); - ASSERT_TRUE(cs->GetNsClient()->DropProcedure("test2", "demo2", msg)); - ASSERT_TRUE(cs->GetNsClient()->DropProcedure("test2", "demo3", msg)); - ASSERT_TRUE(cs->GetNsClient()->DropTable("test2", "trans", msg)); - ASSERT_TRUE(cs->GetNsClient()->DropDatabase("test2", msg)); + // drop deployment + sr->ExecuteSQL("test2", "drop deployment demo1;", &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); + rs = sr->ExecuteSQL("test2", "drop deployment demo2;", &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); + rs = sr->ExecuteSQL("test2", "drop deployment demo3;", &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); + + sr->ExecuteSQL("test2", "drop table trans", &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); + sr->ExecuteSQL("drop database test2;", &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); } void CreateDBTableForLongWindow(const std::string& base_db, const std::string& base_table) { @@ -1711,6 +1710,7 @@ void PrepareRequestRowForLongWindow(const std::string& base_db, const std::strin } // TODO(ace): create instance of DeployLongWindowEnv with template +static absl::BitGen gen; // reseed may segfault, use one for all env class DeployLongWindowEnv { public: explicit DeployLongWindowEnv(sdk::SQLClusterRouter* sr) : sr_(sr) {} @@ -1718,9 +1718,9 @@ class DeployLongWindowEnv { virtual ~DeployLongWindowEnv() {} void SetUp() { - db_ = absl::StrCat("db_", absl::Uniform(gen_, 0, std::numeric_limits::max())); - table_ = absl::StrCat("tb_", absl::Uniform(gen_, 0, std::numeric_limits::max())); - dp_ = absl::StrCat("dp_", absl::Uniform(gen_, 0, std::numeric_limits::max())); + db_ = absl::StrCat("db_", absl::Uniform(gen, 0, std::numeric_limits::max())); + table_ = absl::StrCat("tb_", absl::Uniform(gen, 0, std::numeric_limits::max())); + dp_ = absl::StrCat("dp_", absl::Uniform(gen, 0, std::numeric_limits::max())); PrepareSchema(); @@ -1732,7 +1732,7 @@ class DeployLongWindowEnv { } void TearDown() { - TearDownPreAggTables(); + TearDownDeployment(); ProcessSQLs(sr_, { absl::StrCat("drop table ", table_), absl::StrCat("drop database ", db_), @@ -1788,7 +1788,12 @@ class DeployLongWindowEnv { virtual void Deploy() = 0; - virtual void TearDownPreAggTables() = 0; + virtual void TearDownDeployment() { + ProcessSQLs(sr_, { + absl::StrCat("use ", db_), + absl::StrCat("drop deployment ", dp_), + }); + } void GetRequestRow(std::shared_ptr* rs, const std::string& name) { // NOLINT ::hybridse::sdk::Status status; @@ -1814,7 +1819,6 @@ class DeployLongWindowEnv { protected: sdk::SQLClusterRouter* sr_; - absl::BitGen gen_; std::string db_; std::string table_; std::string dp_; @@ -1843,14 +1847,14 @@ TEST_P(DBSDKTest, DeployLongWindowsWithDataFail) { ".col2 ORDER BY col3" " ROWS_RANGE BETWEEN 5 PRECEDING AND CURRENT ROW);"; sr->ExecuteSQL(base_db, "use " + base_db + ";", &status); - ASSERT_TRUE(status.IsOK()) << status.msg; + ASSERT_TRUE(status.IsOK()) << status.ToString(); sr->ExecuteSQL(base_db, deploy_sql, &status); ASSERT_TRUE(!status.IsOK()); ok = sr->ExecuteDDL(base_db, "drop table " + base_table + ";", &status); - ASSERT_TRUE(ok) << status.msg; + ASSERT_TRUE(ok) << status.ToString(); ok = sr->DropDB(base_db, &status); - ASSERT_TRUE(ok); + ASSERT_TRUE(ok) << status.ToString(); } TEST_P(DBSDKTest, DeployLongWindowsEmpty) { @@ -2871,25 +2875,6 @@ TEST_P(DBSDKTest, DeployLongWindowsExecuteCountWhere2) { w1 AS (PARTITION BY col1,col2 ORDER BY col3 ROWS_RANGE BETWEEN 6s PRECEDING AND CURRENT ROW);)", dp_, table_)}); } - - void TearDownPreAggTables() override { - absl::string_view pre_agg_db = openmldb::nameserver::PRE_AGG_DB; - ProcessSQLs(sr_, { - absl::StrCat("use ", pre_agg_db), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_i64_col_i64_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_i64_col_i16_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_i16_col_i32_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_i32_col_f_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_f_col_d_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_d_col_d_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_s_col_col1"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_date_col_s_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where__i64_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_filter_i64_col"), - absl::StrCat("use ", db_), - absl::StrCat("drop deployment ", dp_), - }); - } }; // request window [5s, 11s] @@ -2948,24 +2933,6 @@ TEST_P(DBSDKTest, DeployLongWindowsExecuteCountWhere3) { w2 AS (PARTITION BY col1,col2 ORDER BY i64_col ROWS BETWEEN 6 PRECEDING AND CURRENT ROW);)", dp_, table_)}); } - - void TearDownPreAggTables() override { - absl::string_view pre_agg_db = openmldb::nameserver::PRE_AGG_DB; - ProcessSQLs(sr_, { - absl::StrCat("use ", pre_agg_db), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_i64_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_i64_col_col1"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_i16_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_i32_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_f_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_d_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_t_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_s_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_count_where_date_col_filter"), - absl::StrCat("use ", db_), - absl::StrCat("drop deployment ", dp_), - }); - } }; // request window [4s, 11s] @@ -3023,26 +2990,6 @@ TEST_P(DBSDKTest, LongWindowMinMaxWhere) { w1 AS (PARTITION BY col1,col2 ORDER BY col3 ROWS_RANGE BETWEEN 7s PRECEDING AND CURRENT ROW))s", dp_, table_)}); } - - void TearDownPreAggTables() override { - absl::string_view pre_agg_db = openmldb::nameserver::PRE_AGG_DB; - ProcessSQLs(sr_, { - absl::StrCat("use ", pre_agg_db), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_max_where_i64_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_max_where_i64_col_col1"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_max_where_i16_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_max_where_i32_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_max_where_f_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_max_where_d_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_min_where_i64_col_i16_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_min_where_i16_col_i32_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_min_where_i32_col_f_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_min_where_f_col_d_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_min_where_d_col_d_col"), - absl::StrCat("use ", db_), - absl::StrCat("drop deployment ", dp_), - }); - } }; // request window [4s, 11s] @@ -3100,25 +3047,6 @@ TEST_P(DBSDKTest, LongWindowSumWhere) { w1 AS (PARTITION BY col1,col2 ORDER BY col3 ROWS_RANGE BETWEEN 7s PRECEDING AND CURRENT ROW))s", dp_, table_)}); } - - void TearDownPreAggTables() override { - absl::string_view pre_agg_db = openmldb::nameserver::PRE_AGG_DB; - ProcessSQLs(sr_, { - absl::StrCat("use ", pre_agg_db), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_i64_col_col1"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_i16_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_i32_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_f_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_d_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_i64_col_i16_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_i16_col_i32_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_i32_col_f_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_f_col_d_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_sum_where_d_col_d_col"), - absl::StrCat("use ", db_), - absl::StrCat("drop deployment ", dp_), - }); - } }; // request window [4s, 11s] @@ -3175,25 +3103,6 @@ TEST_P(DBSDKTest, LongWindowAvgWhere) { w1 AS (PARTITION BY col1,col2 ORDER BY col3 ROWS_RANGE BETWEEN 7s PRECEDING AND CURRENT ROW))s", dp_, table_)}); } - - void TearDownPreAggTables() override { - absl::string_view pre_agg_db = openmldb::nameserver::PRE_AGG_DB; - ProcessSQLs(sr_, { - absl::StrCat("use ", pre_agg_db), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i64_col_col1"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i16_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i32_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_f_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_d_col_f_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i64_col_i16_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i16_col_i32_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i32_col_f_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_f_col_d_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_d_col_d_col"), - absl::StrCat("use ", db_), - absl::StrCat("drop deployment ", dp_), - }); - } }; // request window [4s, 11s] @@ -3273,25 +3182,6 @@ TEST_P(DBSDKTest, LongWindowAnyWhereWithDataOutOfOrder) { ASSERT_TRUE(ok && s.IsOK()) << s.msg << "\n" << s.trace; } } - - void TearDownPreAggTables() override { - absl::string_view pre_agg_db = openmldb::nameserver::PRE_AGG_DB; - ProcessSQLs(sr_, { - absl::StrCat("use ", pre_agg_db), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i64_col_col1"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i16_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i32_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_f_col_filter"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_d_col_f_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i64_col_i16_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i16_col_i32_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_i32_col_f_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_f_col_d_col"), - absl::StrCat("drop table pre_", db_, "_", dp_, "_w1_avg_where_d_col_d_col"), - absl::StrCat("use ", db_), - absl::StrCat("drop deployment ", dp_), - }); - } }; // request window [4s, 11s] @@ -3354,8 +3244,7 @@ TEST_P(DBSDKTest, LongWindowAnyWhereUnsupportRowsBucket) { << "code=" << status.code << ", msg=" << status.msg << "\n" << status.trace; } - - void TearDownPreAggTables() override {} + void TearDownDeployment() override {} }; // unsupport: deploy any_where with rows bucket @@ -3390,8 +3279,7 @@ TEST_P(DBSDKTest, LongWindowAnyWhereUnsupportTimeFilter) { << "code=" << status.code << ", msg=" << status.msg << "\n" << status.trace; } - - void TearDownPreAggTables() override {} + void TearDownDeployment() override {} }; DeployLongWindowAnyWhereEnv env(sr); @@ -3420,8 +3308,7 @@ TEST_P(DBSDKTest, LongWindowAnyWhereUnsupportTimeFilter) { << "code=" << status.code << ", msg=" << status.msg << "\n" << status.trace; } - - void TearDownPreAggTables() override {} + void TearDownDeployment() override {} }; DeployLongWindowAnyWhereEnv env(sr); @@ -3481,8 +3368,7 @@ TEST_P(DBSDKTest, LongWindowAnyWhereUnsupportHDDTable) { << "code=" << status.code << ", msg=" << status.msg << "\n" << status.trace; } - - void TearDownPreAggTables() override {} + void TearDownDeployment() override {} }; DeployLongWindowAnyWhereEnv env(sr); @@ -3504,26 +3390,35 @@ TEST_P(DBSDKTest, LongWindowsCleanup) { " max(c5) over w2 as w2_max_c5 FROM trans" " WINDOW w1 AS (PARTITION BY trans.c1 ORDER BY trans.c7 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)," " w2 AS (PARTITION BY trans.c1 ORDER BY trans.c4 ROWS BETWEEN 3 PRECEDING AND CURRENT ROW);"; + for (int i = 0; i < 10; i++) { HandleSQL("create database test2;"); HandleSQL("use test2;"); HandleSQL(create_sql); + LOG(INFO) << "before deploy " << i; + HandleSQL("select * from __INTERNAL_DB.PRE_AGG_META_INFO;"); sr->ExecuteSQL(deploy_sql, &status); ASSERT_TRUE(status.IsOK()); + absl::SleepFor(absl::Seconds(3)); + LOG(INFO) << "after deploy " << i; + HandleSQL("select * from __INTERNAL_DB.PRE_AGG_META_INFO;"); std::string msg; std::string result_sql = "select * from __INTERNAL_DB.PRE_AGG_META_INFO;"; auto rs = sr->ExecuteSQL("", result_sql, &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); ASSERT_EQ(2, rs->Size()); - auto ok = sr->ExecuteDDL(openmldb::nameserver::PRE_AGG_DB, "drop table pre_test2_demo1_w1_sum_c4;", &status); - ASSERT_TRUE(ok); - ok = sr->ExecuteDDL(openmldb::nameserver::PRE_AGG_DB, "drop table pre_test2_demo1_w2_max_c5;", &status); - ASSERT_TRUE(ok); + sr->ExecuteSQL("test2", "drop table trans;", &status); + ASSERT_FALSE(status.IsOK()); + sr->ExecuteSQL("drop procedure demo1;", &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); + sr->ExecuteSQL("test2", "drop table trans;", &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); + result_sql = "select * from __INTERNAL_DB.PRE_AGG_META_INFO;"; + HandleSQL(result_sql); rs = sr->ExecuteSQL("", result_sql, &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); ASSERT_EQ(0, rs->Size()); - ASSERT_FALSE(cs->GetNsClient()->DropTable("test2", "trans", msg)); - ASSERT_TRUE(cs->GetNsClient()->DropProcedure("test2", "demo1", msg)) << msg; - ASSERT_TRUE(cs->GetNsClient()->DropTable("test2", "trans", msg)) << msg; // helpful for debug HandleSQL("show tables;"); HandleSQL("show deployments;"); @@ -4151,6 +4046,7 @@ TEST_F(SqlCmdTest, SelectWithAddNewIndex) { hybridse::sdk::Status status; auto res = sr->ExecuteSQL(absl::StrCat("use ", db1_name, ";"), &status); res = sr->ExecuteSQL(absl::StrCat("select id,c1,c2,c3 from ", tb1_name), &status); + ASSERT_TRUE(status.IsOK()) << status.ToString(); ASSERT_EQ(res->Size(), 4); res = sr->ExecuteSQL(absl::StrCat("select id,c1,c2,c3 from ", tb1_name, " where c1='aa';"), &status); ASSERT_EQ(res->Size(), 3); diff --git a/src/sdk/sql_cluster_router.cc b/src/sdk/sql_cluster_router.cc index 8ef74f8ac2d..0a77681668d 100644 --- a/src/sdk/sql_cluster_router.cc +++ b/src/sdk/sql_cluster_router.cc @@ -880,54 +880,38 @@ bool SQLClusterRouter::DropTable(const std::string& db, const std::string& table } } - // delete pre-aggr meta info if need - if (table_info->base_table_tid() > 0) { - std::string meta_db = openmldb::nameserver::INTERNAL_DB; - std::string meta_table = openmldb::nameserver::PRE_AGG_META_NAME; - std::string select_aggr_info = - absl::StrCat("select base_db,base_table,aggr_func,aggr_col,partition_cols,order_by_col,filter_col from ", - meta_db, ".", meta_table, " where aggr_table = '", table_info->name(), "';"); - auto rs = ExecuteSQL("", select_aggr_info, true, true, 0, status); - WARN_NOT_OK_AND_RET(status, "get aggr info failed", false); - if (rs->Size() != 1) { - SET_STATUS_AND_WARN(status, StatusCode::kCmdError, - "duplicate records generate with aggr table name: " + table_info->name()); - return false; - } - std::string idx_key; - if (rs->Next()) { - for (int i = 0; i < rs->GetSchema()->GetColumnCnt(); i++) { - if (!idx_key.empty()) { - idx_key += "|"; - } - auto k = rs->GetAsStringUnsafe(i); - if (k.empty()) { - idx_key += hybridse::codec::EMPTY_STRING; - } else { - idx_key += k; - } + // delete related pre-aggr tables first + std::string meta_db = openmldb::nameserver::INTERNAL_DB; + std::string meta_table = openmldb::nameserver::PRE_AGG_META_NAME; + std::string select_aggr_info = + absl::StrCat("select aggr_db, aggr_table from ", meta_db, ".", meta_table, " where base_table = '", + table_info->name(), "' and base_db='", table_info->db(), "';"); + auto rs = ExecuteSQL("", select_aggr_info, true, true, 0, status); + WARN_NOT_OK_AND_RET(status, "get aggr info failed", false); + if (rs->Size() > 0) { + // drop aggr-table and meta info one by one, if failed, plz delete manually + while (rs->Next()) { + std::string aggr_db = rs->GetStringUnsafe(0); + std::string aggr_table = rs->GetStringUnsafe(1); + + if (aggr_db.empty() || aggr_table.empty()) { + WARN_NOT_OK_AND_RET( + status, absl::StrCat("aggr table ", aggr_db, " or ", aggr_table, " is empty, can't delete"), false); } - } else { - SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "access ResultSet failed"); - return false; - } - auto tablet_accessor = cluster_sdk_->GetTablet(meta_db, meta_table, (uint32_t)0); - if (!tablet_accessor) { - SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "get tablet accessor failed"); - return false; - } - auto tablet_client = tablet_accessor->GetClient(); - if (!tablet_client) { - SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "get tablet client failed"); - return false; - } - auto tid = cluster_sdk_->GetTableId(meta_db, meta_table); - std::string msg; - if (!tablet_client->Delete(tid, 0, table_info->name(), "aggr_table", msg) || - !tablet_client->Delete(tid, 0, idx_key, "unique_key", msg)) { - SET_STATUS_AND_WARN(status, StatusCode::kCmdError, "delete aggr meta failed"); - return false; + if (!DropTable(aggr_db, aggr_table, true, status)) { + WARN_NOT_OK_AND_RET(status, absl::StrCat("drop aggr table ", aggr_db, ".", aggr_table, " failed"), + false); + } + LOG(INFO) << "drop aggr meta " << aggr_db << "." << aggr_table << "by table name"; + // Ref CheckPreAggrTableExist, checking existence of aggr table uses the unique_key index, ignore bucket_size. + // But for deleting, we don't need to consider the unique_key. Just delete the table, aggr_table is unique. + std::string delete_aggr_info = + absl::StrCat("delete from ", meta_db, ".", meta_table, " where aggr_table='", aggr_table, "';"); + auto rs = ExecuteSQL("", delete_aggr_info, true, true, 0, status); + WARN_NOT_OK_AND_RET(status, "delete aggr info failed", false); } + } else { + LOG(INFO) << "no related pre-aggr tables"; } // Check offline table info first @@ -3046,7 +3030,6 @@ std::shared_ptr SQLClusterRouter::ExecuteSQL( case hybridse::node::kPlanTypeCreateUser: { auto create_node = dynamic_cast(node); UserInfo user_info; - auto result = GetUser(create_node->Name(), &user_info); if (!result.ok()) { *status = {StatusCode::kCmdError, result.status().message()}; From cd905dac3e4eaf21f61c1de5b106bad34e4c1626 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Mon, 8 Jul 2024 17:44:03 +0800 Subject: [PATCH 28/41] ci(#3954): fix checkout action on old glibc OS (#3955) * ci(#3954): fix checkout action on old glibc OS * ci: include checkout fix in all workflows * ci: fix python-sdk --- .github/workflows/cicd.yaml | 2 ++ .github/workflows/devops-test.yml | 2 ++ .github/workflows/hybridse-ci.yml | 2 ++ .github/workflows/integration-test-pkg.yml | 2 ++ .github/workflows/integration-test-src.yml | 2 ++ .github/workflows/integration-test.yml | 2 ++ .github/workflows/openmldb-tool.yml | 2 ++ .github/workflows/sdk.yml | 4 ++++ .github/workflows/selfhost_intergration.yml | 2 ++ 9 files changed, 20 insertions(+) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index ca4f0bc7f50..33217bb71fb 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -46,6 +46,8 @@ jobs: TESTING_ENABLE: ON NPROC: 8 CTEST_PARALLEL_LEVEL: 1 # parallel test level for ctest (make test) + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/devops-test.yml b/.github/workflows/devops-test.yml index d139c0f8bdc..7f85941d8e9 100644 --- a/.github/workflows/devops-test.yml +++ b/.github/workflows/devops-test.yml @@ -15,6 +15,8 @@ on: env: GIT_SUBMODULE_STRATEGY: recursive HYBRIDSE_SOURCE: local + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true jobs: node-failure-test-cluster: diff --git a/.github/workflows/hybridse-ci.yml b/.github/workflows/hybridse-ci.yml index 7da8e5ac100..9ea250baf27 100644 --- a/.github/workflows/hybridse-ci.yml +++ b/.github/workflows/hybridse-ci.yml @@ -29,6 +29,8 @@ jobs: image: ghcr.io/4paradigm/hybridsql:latest env: TESTING_ENABLE_STRIP: ON + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/integration-test-pkg.yml b/.github/workflows/integration-test-pkg.yml index 2116263d92e..2c5ea196d07 100644 --- a/.github/workflows/integration-test-pkg.yml +++ b/.github/workflows/integration-test-pkg.yml @@ -31,6 +31,8 @@ on: env: GIT_SUBMODULE_STRATEGY: recursive HYBRIDSE_SOURCE: + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true jobs: # java-sdk-test-standalone-0: diff --git a/.github/workflows/integration-test-src.yml b/.github/workflows/integration-test-src.yml index d6fd1cfa526..381e74eb76b 100644 --- a/.github/workflows/integration-test-src.yml +++ b/.github/workflows/integration-test-src.yml @@ -13,6 +13,8 @@ on: env: GIT_SUBMODULE_STRATEGY: recursive HYBRIDSE_SOURCE: local + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true jobs: # java-sdk-test-standalone-0: diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index d68bae2465d..af689f2fbc9 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -25,6 +25,8 @@ env: NPROC: 5 # default Parallel build number for GitHub's Linux runner EXAMPLES_ENABLE: OFF # turn off hybridse's example code HYBRIDSE_TESTING_ENABLE: OFF # turn off hybridse's test code + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true jobs: openmldb-test-python: diff --git a/.github/workflows/openmldb-tool.yml b/.github/workflows/openmldb-tool.yml index 23d3f5b1dfd..4cc38133e05 100644 --- a/.github/workflows/openmldb-tool.yml +++ b/.github/workflows/openmldb-tool.yml @@ -20,6 +20,8 @@ env: GIT_SUBMODULE_STRATEGY: recursive DEPLOY_DIR: /mnt/hdd0/openmldb_runner_work/openmldb_env NODE_LIST: node-1,node-2,node-3 + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true jobs: openmldb-tool-test: diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index b6c180111a4..c24f32628e3 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -44,6 +44,8 @@ jobs: OPENMLDB_BUILD_TARGET: "cp_native_so openmldb" MAVEN_OPTS: -Duser.home=/github/home SPARK_HOME: /tmp/spark/ + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: - uses: actions/checkout@v2 @@ -259,6 +261,8 @@ jobs: env: SQL_PYSDK_ENABLE: ON OPENMLDB_BUILD_TARGET: "cp_python_sdk_so openmldb" + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/selfhost_intergration.yml b/.github/workflows/selfhost_intergration.yml index 87de8536daf..6a958206c93 100644 --- a/.github/workflows/selfhost_intergration.yml +++ b/.github/workflows/selfhost_intergration.yml @@ -20,6 +20,8 @@ env: E_VERSION: ${{ github.event.inputs.EXEC_VERSION || 'main'}} ETYPE: ${{ github.event.inputs.EXEC_TEST_TYPE || 'all'}} NPROC: 4 + # ref https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/ + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true jobs: build-openmldb: From b6ffe033672b682ccf39064360482306c7c8869a Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Thu, 11 Jul 2024 14:24:39 +0800 Subject: [PATCH 29/41] test: node-2 to node-3 (#3957) node-3 is not available, moving to node-2 --- test/steps/format_config.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/steps/format_config.sh b/test/steps/format_config.sh index fb8dfd47c24..eb2f60d52ba 100755 --- a/test/steps/format_config.sh +++ b/test/steps/format_config.sh @@ -12,7 +12,7 @@ curTime=$(date "+%m%d%H%M") dirName="${jobName}-${version}-${curTime}" #set Deploy Host and Ports -Hosts=(node-3 node-4 node-1) +Hosts=(node-2 node-4 node-1) AvaNode1Ports=$(ssh "${Hosts[0]}" "comm -23 <(seq $portFrom $portTo | sort) <(/usr/sbin/ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | shuf | head -n 8") AvaNode2Ports=$(ssh "${Hosts[1]}" "comm -23 <(seq $portFrom $portTo | sort) <(/usr/sbin/ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | shuf | head -n 2") From 9a816836a804dcb80ed90ffa178a5604a21c7f67 Mon Sep 17 00:00:00 2001 From: howd <81472844+howdb@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:51:10 +0800 Subject: [PATCH 30/41] feat: support locate(substr, str[, pos]) function(#820) (#3943) --- hybridse/src/codegen/udf_ir_builder_test.cc | 24 ++++++++++++ hybridse/src/udf/default_udf_library.cc | 41 +++++++++++++++++++++ hybridse/src/udf/udf.cc | 36 ++++++++++++++++++ hybridse/src/udf/udf.h | 2 + 4 files changed, 103 insertions(+) diff --git a/hybridse/src/codegen/udf_ir_builder_test.cc b/hybridse/src/codegen/udf_ir_builder_test.cc index 6cd82be7859..91a56b83c49 100644 --- a/hybridse/src/codegen/udf_ir_builder_test.cc +++ b/hybridse/src/codegen/udf_ir_builder_test.cc @@ -766,6 +766,30 @@ TEST_F(UdfIRBuilderTest, SubstringPosUdfTest) { StringRef("1234567890"), -12); } +TEST_F(UdfIRBuilderTest, LocateUdfTest) { + CheckUdf("locate", 1, StringRef("ab"), StringRef("abcab")); + CheckUdf("locate", 3, StringRef("ab"), StringRef("bcab")); + CheckUdf("locate", 0, StringRef("ab"), StringRef("bcAb")); + CheckUdf("locate", 1, StringRef(""), StringRef("")); +} + +TEST_F(UdfIRBuilderTest, LocatePosUdfTest) { + CheckUdf("locate", 0, StringRef("ab"), StringRef("ab"), -1); + CheckUdf("locate", 0, StringRef("ab"), StringRef("Ab"), 1); + + CheckUdf("locate", 4, StringRef("ab"), StringRef("abcab"), 2); + CheckUdf("locate", 0, StringRef("ab"), StringRef("abcAb"), 2); + CheckUdf("locate", 4, StringRef("ab"), StringRef("abcab"), 2); + CheckUdf("locate", 0, StringRef("ab"), StringRef("abcab"), 6); + + CheckUdf("locate", 5, StringRef(""), StringRef("abcab"), 5); + CheckUdf("locate", 6, StringRef(""), StringRef("abcab"), 6); + CheckUdf("locate", 0, StringRef(""), StringRef("abcab"), 7); + + CheckUdf("locate", 1, StringRef(""), StringRef(""), 1); + CheckUdf("locate", 0, StringRef(""), StringRef(""), 2); +} + TEST_F(UdfIRBuilderTest, UpperUcase) { CheckUdf, Nullable>("upper", StringRef("SQL"), StringRef("Sql")); CheckUdf, Nullable>("ucase", StringRef("SQL"), StringRef("Sql")); diff --git a/hybridse/src/udf/default_udf_library.cc b/hybridse/src/udf/default_udf_library.cc index d6fed696ab3..a6be6745917 100644 --- a/hybridse/src/udf/default_udf_library.cc +++ b/hybridse/src/udf/default_udf_library.cc @@ -911,6 +911,47 @@ void DefaultUdfLibrary::InitStringUdf() { RegisterAlias("substr", "substring"); + RegisterExternal("locate") + .args( + static_cast(udf::v1::locate)) + .doc(R"( + @brief Returns the position of the first occurrence of substr in str. The given pos and return value are 1-based. + This is a version of the `locate` function where `pos` has a default value of 1. + + Example: + + @code{.sql} + + select locate("wo", "hello world"); + --output 7 + + @endcode)"); + + RegisterExternal("locate") + .args( + static_cast(udf::v1::locate)) + .doc(R"( + @brief Returns the position of the first occurrence of substr in str after position pos. The given pos and return value are 1-based. + + Example: + + @code{.sql} + + select locate("wo", "hello world", 2); + --output 7 + + select locate("Wo", "hello world", 2); + --output 0 + + @endcode + + @param substr + @param str + @param pos: define the begining search position of the str. + - Negetive value is illegal and will return 0 directly; + - If substr is "" and pos less equal len(str) + 1, return pos, other case return 0; + )"); + RegisterExternal("strcmp") .args( static_cast( diff --git a/hybridse/src/udf/udf.cc b/hybridse/src/udf/udf.cc index e5faf25db1e..2fe393cf12b 100644 --- a/hybridse/src/udf/udf.cc +++ b/hybridse/src/udf/udf.cc @@ -1085,6 +1085,42 @@ void sub_string(StringRef *str, int32_t from, int32_t len, output->size_ = static_cast(len); return; } + +int32_t locate(StringRef *substr, StringRef *str) { + return locate(substr, str, 1); +} + +int32_t locate(StringRef *substr, StringRef *str, int32_t pos) { + if (nullptr == substr || nullptr == str) { + return 0; + } + // negetive pos return 0 directly + if (pos <= 0) { + return 0; + } + uint32_t sub_size = substr->size_; + uint32_t size = str->size_; + // if substr is "" and pos <= len(str) + 1, return pos, other case return 0 + if (pos + sub_size - 1 > size) { + return 0; + } + if (sub_size == 0) { + return pos; + } + for (uint32_t i = pos - 1; i <= size - sub_size; i++) { + uint32_t j = 0, k = i; + for (; j < sub_size; j++, k++) { + if (str->data_[k] != substr->data_[j]) { + break; + } + } + if (j == sub_size) { + return i + 1; + } + } + return 0; +} + int32_t strcmp(StringRef *s1, StringRef *s2) { if (s1 == s2) { return 0; diff --git a/hybridse/src/udf/udf.h b/hybridse/src/udf/udf.h index 480e4f89f3c..554db565b0f 100644 --- a/hybridse/src/udf/udf.h +++ b/hybridse/src/udf/udf.h @@ -390,6 +390,8 @@ void sub_string(StringRef *str, int32_t pos, StringRef *output); void sub_string(StringRef *str, int32_t pos, int32_t len, StringRef *output); +int32_t locate(StringRef *substr, StringRef* str); +int32_t locate(StringRef *substr, StringRef* str, int32_t pos); int32_t strcmp(StringRef *s1, StringRef *s2); void bool_to_string(bool v, StringRef *output); From c2a754fcc3babdc5aa523bc45d421284a695d7c7 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Fri, 12 Jul 2024 13:25:36 +0800 Subject: [PATCH 31/41] fix(scripts): deploy spark correctly (#3958) $SPARK_HOME may be a symbolic link referring to a invalid directory, so we'd try 'rm -f' first --- release/sbin/deploy-all.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/release/sbin/deploy-all.sh b/release/sbin/deploy-all.sh index ddfc7e712cd..3fad5fb14f0 100755 --- a/release/sbin/deploy-all.sh +++ b/release/sbin/deploy-all.sh @@ -128,7 +128,12 @@ function download_spark { if [[ -z "${SPARK_HOME}" ]]; then echo "[ERROR] SPARK_HOME is not set" else - if [[ ! -e "${SPARK_HOME}" ]]; then + if [[ ! -d "${SPARK_HOME}" ]]; then + # SPARK_HOME may be symbolic link, -d will consider only directory exists, + # this filter out existing symbolic link refer to a non-exists directory. + # And we'd try rm it first + rm -fv "${SPARK_HOME}" + echo "Downloading openmldbspark..." spark_name=spark-3.2.1-bin-openmldbspark spark_tar="${spark_name}".tgz From 60e4fb5fcbe497257cd6d532bf87aa1e7e0d90c1 Mon Sep 17 00:00:00 2001 From: tobe Date: Thu, 18 Jul 2024 09:20:13 +0800 Subject: [PATCH 32/41] Add changelog for 0.9.1 (#3959) --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f74677387f..6afc9e2df69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## [0.9.1] - 2024-07-17 + +### Features +- Support merge DAG SQL in Java SDK (#3911 @wyl4pd) +- Support tablets get user table remotely (#3918 @oh2024) +- Support set global variable @@execute_mode = 'request' (#3924 @aceforeverd) +- Support crud users synchronously (#3928 @oh2024) +- Support simple ANSI SQL rewriter(#3934 @aceforeverd) +- Support execute mode batchrequest(#3938 @aceforeverd) +- Support new UDF of isin, array_combine, array_join and locate, (#3939 #3945 @aceforeverd #3940 #3943 @howdb) +- Support server side authorization (#3941 @oh2024) +- Support new index type and IoT Table (#3944 @vagetablechicken) + +### Bug Fixes +- Fix setup script uses incorrect ZooKeeper configuration file(#3901 @greatljn) +- Fix make clients use always send auth info(#3906 @oh2024) +- Fix drop aggr tables in drop table (#3908 @vagetablechicken) +- Fix checkout execute_mode in config clause in sql client (#3909 @aceforeverd) +- Fix continuous sign for gcformat with space (#3921 @wyl4pd) +- Fix package issues by removing s3 dependencies and fix package conflict of curator (#3929 @tobegit3hub) +- Fix repeated sort keys and sort as int(#3947 @oh2024) +- Fix checkout action on old glibc OS (#3955 @aceforeverd) +- Fix CICD issue of deploying spark correctly(#3958 @aceforeverd) + +### Testing +- Set NPROC in intergration test(#3782 @dl239) +- Support map data type in yaml testing framework(#3765 @aceforeverd) +- Add automatic table cleanup after go sdk tests(#3799 @oh2024) +- Fix sql_cmd_test and no impl for MakeMergeNode(#3829 @aceforeverd) +- Add query performance benchmark(#3855 @gaoboal) + ## [0.9.0] - 2024-04-25 ### Breaking Changes From 4138c1b6c9caba78cf3bdf04ca7c052f4d3bb6e4 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Tue, 23 Jul 2024 18:50:01 +0800 Subject: [PATCH 33/41] fix: select from JOB_INFO should always in online mode (#3963) * fix: select from JOB_INFO should always in online mode Fix error when user set default `execute_mode` to offline: ```sql set global execute_mode = 'offline'; select 1; ``` * fix: query mode on user & pre_agg tables --- .../openmldb/taskmanager/JobInfoManager.scala | 8 ++++---- src/sdk/sql_cluster_router.cc | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/java/openmldb-taskmanager/src/main/scala/com/_4paradigm/openmldb/taskmanager/JobInfoManager.scala b/java/openmldb-taskmanager/src/main/scala/com/_4paradigm/openmldb/taskmanager/JobInfoManager.scala index 47f1afb4d7b..7f6cd9c49b8 100644 --- a/java/openmldb-taskmanager/src/main/scala/com/_4paradigm/openmldb/taskmanager/JobInfoManager.scala +++ b/java/openmldb-taskmanager/src/main/scala/com/_4paradigm/openmldb/taskmanager/JobInfoManager.scala @@ -73,7 +73,7 @@ object JobInfoManager { } def getAllJobs(): List[JobInfo] = { - val sql = s"SELECT * FROM $JOB_INFO_TABLE_NAME" + val sql = s"SELECT * FROM $JOB_INFO_TABLE_NAME CONFIG (execute_mode = 'online')" val rs = sqlExecutor.executeSQL(INTERNAL_DB_NAME, sql) // TODO: Reorder in output, use orderby desc if SQL supported resultSetToJobs(rs).sortWith(_.getId > _.getId) @@ -82,7 +82,7 @@ object JobInfoManager { def getUnfinishedJobs(): List[JobInfo] = { // TODO: Now we can not add index for `state` and run sql with // s"SELECT * FROM $tableName WHERE state NOT IN (${JobInfo.FINAL_STATE.mkString(",")})" - val sql = s"SELECT * FROM $JOB_INFO_TABLE_NAME" + val sql = s"SELECT * FROM $JOB_INFO_TABLE_NAME CONFIG (execute_mode = 'online')" val rs = sqlExecutor.executeSQL(INTERNAL_DB_NAME, sql) val jobs = mutable.ArrayBuffer[JobInfo]() @@ -99,7 +99,7 @@ object JobInfoManager { } def stopJob(jobId: Int): JobInfo = { - val sql = s"SELECT * FROM $JOB_INFO_TABLE_NAME WHERE id = $jobId" + val sql = s"SELECT * FROM $JOB_INFO_TABLE_NAME WHERE id = $jobId CONFIG (execute_mode = 'online')" val rs = sqlExecutor.executeSQL(INTERNAL_DB_NAME, sql) val jobInfo = if (rs.getFetchSize == 0) { @@ -131,7 +131,7 @@ object JobInfoManager { def getJob(jobId: Int): Option[JobInfo] = { // TODO: Require to get only one row, https://github.com/4paradigm/OpenMLDB/issues/704 - val sql = s"SELECT * FROM $JOB_INFO_TABLE_NAME WHERE id = $jobId" + val sql = s"SELECT * FROM $JOB_INFO_TABLE_NAME WHERE id = $jobId CONFIG (execute_mode = 'online')" val rs = sqlExecutor.executeSQL(INTERNAL_DB_NAME, sql) if (rs.getFetchSize == 0) { diff --git a/src/sdk/sql_cluster_router.cc b/src/sdk/sql_cluster_router.cc index 0a77681668d..607dd1c85b7 100644 --- a/src/sdk/sql_cluster_router.cc +++ b/src/sdk/sql_cluster_router.cc @@ -885,7 +885,7 @@ bool SQLClusterRouter::DropTable(const std::string& db, const std::string& table std::string meta_table = openmldb::nameserver::PRE_AGG_META_NAME; std::string select_aggr_info = absl::StrCat("select aggr_db, aggr_table from ", meta_db, ".", meta_table, " where base_table = '", - table_info->name(), "' and base_db='", table_info->db(), "';"); + table_info->name(), "' and base_db='", table_info->db(), "' CONFIG (execute_mode = 'online');"); auto rs = ExecuteSQL("", select_aggr_info, true, true, 0, status); WARN_NOT_OK_AND_RET(status, "get aggr info failed", false); if (rs->Size() > 0) { @@ -5143,7 +5143,7 @@ void SQLClusterRouter::ReadSparkConfFromFile(std::string conf_file_path, std::ma std::shared_ptr SQLClusterRouter::GetJobResultSet(int job_id, ::hybridse::sdk::Status* status) { std::string db = openmldb::nameserver::INTERNAL_DB; - std::string sql = "SELECT * FROM JOB_INFO WHERE id = " + std::to_string(job_id); + std::string sql = absl::Substitute("SELECT * FROM JOB_INFO WHERE id = $0 CONFIG (execute_mode = 'online')", job_id); auto rs = ExecuteSQLParameterized(db, sql, {}, status); if (!status->IsOK()) { @@ -5164,7 +5164,7 @@ std::shared_ptr SQLClusterRouter::GetJobResultSet(int std::shared_ptr SQLClusterRouter::GetJobResultSet(::hybridse::sdk::Status* status) { std::string db = openmldb::nameserver::INTERNAL_DB; - std::string sql = "SELECT * FROM JOB_INFO"; + std::string sql = "SELECT * FROM JOB_INFO CONFIG (execute_mode = 'online')"; auto rs = ExecuteSQLParameterized(db, sql, std::shared_ptr(), status); if (!status->IsOK()) { return {}; @@ -5187,7 +5187,7 @@ std::shared_ptr SQLClusterRouter::GetTaskManagerJobRes return this->GetJobResultSet(job_id, status); } std::string db = openmldb::nameserver::INTERNAL_DB; - std::string sql = "SELECT * FROM JOB_INFO;"; + std::string sql = "SELECT * FROM JOB_INFO CONFIG (execute_mode = 'online');"; auto rs = ExecuteSQLParameterized(db, sql, {}, status); if (!status->IsOK()) { return {}; @@ -5226,7 +5226,7 @@ std::shared_ptr SQLClusterRouter::GetNameServerJobResu } absl::StatusOr SQLClusterRouter::GetUser(const std::string& name, UserInfo* user_info) { - std::string sql = absl::StrCat("select * from ", nameserver::USER_INFO_NAME); + std::string sql = absl::StrCat("select * from ", nameserver::USER_INFO_NAME, " CONFIG (execute_mode = 'online')"); hybridse::sdk::Status status; auto rs = ExecuteSQLParameterized(nameserver::INTERNAL_DB, sql, std::shared_ptr(), &status); From 96976f50b53d9b18ed6f35d01be95a91b4e185a9 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Wed, 24 Jul 2024 15:51:16 +0800 Subject: [PATCH 34/41] build(docker): centos7 EOL (#3965) * build(docker): centos7 EOL * fix vault address for aarch64 * ci(docker): disable arm64 image Dont have arm machine to test --- .github/workflows/hybridsql-docker.yml | 2 +- docker/Dockerfile | 10 +++++++--- docker/patch_yum_repo.sh | 11 +++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100755 docker/patch_yum_repo.sh diff --git a/.github/workflows/hybridsql-docker.yml b/.github/workflows/hybridsql-docker.yml index a23786743f6..02d52355f4e 100644 --- a/.github/workflows/hybridsql-docker.yml +++ b/.github/workflows/hybridsql-docker.yml @@ -93,6 +93,6 @@ jobs: with: context: docker push: ${{ github.event_name == 'push' }} - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/docker/Dockerfile b/docker/Dockerfile index aab88ecc4b8..37027cd43cc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -21,9 +21,13 @@ ARG TARGETARCH LABEL org.opencontainers.image.source https://github.com/4paradigm/OpenMLDB -COPY setup_deps.sh / +COPY ./*.sh / # hadolint ignore=DL3031,DL3033 -RUN yum update -y && yum install -y centos-release-scl epel-release && \ +RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo && \ + sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo && \ + sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo && \ + yum update -y && yum install -y centos-release-scl epel-release && \ + /patch_yum_repo.sh && \ yum install -y devtoolset-8 rh-git227 devtoolset-8-libasan-devel flex doxygen java-1.8.0-openjdk-devel rh-python38-python-devel rh-python38-python-wheel rh-python38-python-requests rh-python38-python-pip && \ curl -Lo lcov-1.15-1.noarch.rpm https://github.com/linux-test-project/lcov/releases/download/v1.15/lcov-1.15-1.noarch.rpm && \ yum localinstall -y lcov-1.15-1.noarch.rpm && \ @@ -33,7 +37,7 @@ RUN yum update -y && yum install -y centos-release-scl epel-release && \ tar xzf zookeeper.tar.gz -C /deps/src && \ rm -v ./*.tar.gz && \ /setup_deps.sh -a "$TARGETARCH" -z "$ZETASQL_VERSION" -t "$THIRDPARTY_VERSION" && \ - rm -v /setup_deps.sh + rm -v /*.sh ENV THIRD_PARTY_DIR=/deps/usr ENV THIRD_PARTY_SRC_DIR=/deps/src diff --git a/docker/patch_yum_repo.sh b/docker/patch_yum_repo.sh new file mode 100755 index 00000000000..b771ec2ed53 --- /dev/null +++ b/docker/patch_yum_repo.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo +sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo +sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo + +if [[ "$ARCH" = "aarch64" ]]; then + sed -i s/vault.centos.org\\/centos/vault.centos.org\\/altarch/g /etc/yum.repos.d/*.repo +fi From 2b7d970ebd9d6b4ffedc84e0113416998ef4d117 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Wed, 24 Jul 2024 18:41:38 +0800 Subject: [PATCH 35/41] fix(docker): numpy version lock (#3966) --- demo/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/Dockerfile b/demo/Dockerfile index 90e4317d5b2..6fd4df3bbd7 100644 --- a/demo/Dockerfile +++ b/demo/Dockerfile @@ -16,7 +16,7 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* RUN if [ -f "/additions/pypi.txt" ] ; then pip config set global.index-url $(cat /additions/pypi.txt) ; fi -RUN pip install --no-cache-dir py4j==0.10.9 numpy lightgbm==3 tornado requests pandas==1.5 xgboost==1.4.2 +RUN pip install --no-cache-dir py4j==0.10.9 lightgbm==3 tornado requests pandas==1.5 xgboost==1.4.2 numpy==1.26.4 COPY init.sh /work/ COPY predict-taxi-trip-duration/script /work/taxi-trip/ From b7f048933127b86670c932b315159b0c4c66f307 Mon Sep 17 00:00:00 2001 From: tobe Date: Thu, 25 Jul 2024 10:23:35 +0800 Subject: [PATCH 36/41] Update docs version to 0.9.1 (#3960) --- demo/java_quickstart/demo/pom.xml | 2 +- demo/predict-taxi-trip-duration/README.md | 4 +- .../README.md | 2 +- docs/en/blog_post/20240402_OpenmldbVsRedis.md | 2 +- docs/en/deploy/compile.md | 10 +-- docs/en/deploy/install_deploy.md | 68 +++++++++--------- .../deploy_integration/OpenMLDB_Byzer_taxi.md | 2 +- .../airflow_provider_demo.md | 2 +- .../dolphinscheduler_task_demo.md | 2 +- .../kafka_connector_demo.md | 2 +- .../pulsar_connector_demo.md | 2 +- docs/en/quickstart/openmldb_quickstart.md | 2 +- docs/en/quickstart/sdk/java_sdk.md | 10 +-- docs/en/reference/ip_tips.md | 6 +- docs/en/tutorial/standalone_use.md | 2 +- docs/en/use_case/JD_recommendation.md | 2 +- docs/en/use_case/talkingdata_demo.md | 2 +- .../use_case/taxi_tour_duration_prediction.md | 2 +- docs/zh/blog_post/20240402_OpenmldbVsRedis.md | 2 +- docs/zh/deploy/compile.md | 10 +-- docs/zh/deploy/install_deploy.md | 70 +++++++++---------- .../deploy_integration/OpenMLDB_Byzer_taxi.md | 2 +- .../airflow_provider_demo.md | 2 +- .../dolphinscheduler_task_demo.md | 2 +- .../kafka_connector_demo.md | 2 +- .../pulsar_connector_demo.md | 2 +- docs/zh/quickstart/openmldb_quickstart.md | 2 +- docs/zh/quickstart/sdk/java_sdk.md | 10 +-- docs/zh/reference/ip_tips.md | 12 ++-- docs/zh/tutorial/standalone_use.md | 2 +- docs/zh/use_case/JD_recommendation.md | 2 +- docs/zh/use_case/talkingdata_demo.md | 2 +- .../use_case/taxi_tour_duration_prediction.md | 2 +- release/conf/openmldb-env.sh | 2 +- 34 files changed, 125 insertions(+), 125 deletions(-) diff --git a/demo/java_quickstart/demo/pom.xml b/demo/java_quickstart/demo/pom.xml index 4d05e486276..3af812f8c6e 100644 --- a/demo/java_quickstart/demo/pom.xml +++ b/demo/java_quickstart/demo/pom.xml @@ -29,7 +29,7 @@ com.4paradigm.openmldb openmldb-jdbc - 0.9.0 + 0.9.1 org.testng diff --git a/demo/predict-taxi-trip-duration/README.md b/demo/predict-taxi-trip-duration/README.md index ba537210237..33da4f2086a 100644 --- a/demo/predict-taxi-trip-duration/README.md +++ b/demo/predict-taxi-trip-duration/README.md @@ -28,7 +28,7 @@ w2 as (PARTITION BY passenger_count ORDER BY pickup_datetime ROWS_RANGE BETWEEN **Start docker** ``` -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` **Initialize environment** ```bash @@ -138,7 +138,7 @@ python3 predict.py **Start docker** ```bash -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` **Initialize environment** diff --git a/demo/talkingdata-adtracking-fraud-detection/README.md b/demo/talkingdata-adtracking-fraud-detection/README.md index 085eb37b2a5..f687b7249a9 100644 --- a/demo/talkingdata-adtracking-fraud-detection/README.md +++ b/demo/talkingdata-adtracking-fraud-detection/README.md @@ -15,7 +15,7 @@ We recommend you to use docker to run the demo. OpenMLDB and dependencies have b **Start docker** ``` -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` #### Run locally diff --git a/docs/en/blog_post/20240402_OpenmldbVsRedis.md b/docs/en/blog_post/20240402_OpenmldbVsRedis.md index 19c95361d2a..780e0512835 100644 --- a/docs/en/blog_post/20240402_OpenmldbVsRedis.md +++ b/docs/en/blog_post/20240402_OpenmldbVsRedis.md @@ -44,7 +44,7 @@ We plan to test with 1 million (referred to as 1M) keys, each corresponding to 1 Deployment can be done through containerization or directly on physical machines using software packages. There is no significant difference between the two methods. Below is an example of using containerization for deployment: - OpenMLDB - - Docker image: `docker pull 4pdosc/openmldb:0.9.0` + - Docker image: `docker pull 4pdosc/openmldb:0.9.1` - Documentation: [https://openmldb.ai/docs/zh/main/quickstart/openmldb_quickstart.html](https://openmldb.ai/docs/zh/main/quickstart/openmldb_quickstart.html) - Redis: diff --git a/docs/en/deploy/compile.md b/docs/en/deploy/compile.md index b3659b16ab5..dbf7451da0f 100644 --- a/docs/en/deploy/compile.md +++ b/docs/en/deploy/compile.md @@ -5,7 +5,7 @@ This section describes the steps to compile and use OpenMLDB inside its official docker image [hybridsql](https://hub.docker.com/r/4pdosc/hybridsql), mainly for quick start and development purposes in the docker container. The docker image has packed the required tools and dependencies, so there is no need to set them up separately. To compile without the official docker image, refer to the section [Detailed Instructions for Build](#detailed-instructions-for-build) below. -Keep in mind that you should always use the same version of both compile image and [OpenMLDB version](https://github.com/4paradigm/OpenMLDB/releases). This section demonstrates compiling for [OpenMLDB v0.9.0](https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0) under `hybridsql:0.9.0` ,If you prefer to compile on the latest code in `main` branch, pull `hybridsql:latest` image instead. +Keep in mind that you should always use the same version of both compile image and [OpenMLDB version](https://github.com/4paradigm/OpenMLDB/releases). This section demonstrates compiling for [OpenMLDB v0.9.1](https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.1) under `hybridsql:0.9.1` ,If you prefer to compile on the latest code in `main` branch, pull `hybridsql:latest` image instead. 1. Pull the docker image @@ -19,11 +19,11 @@ Keep in mind that you should always use the same version of both compile image a docker run -it 4pdosc/hybridsql:0.9 bash ``` -3. Download the OpenMLDB source code inside the docker container, and set the branch into v0.9.0 +3. Download the OpenMLDB source code inside the docker container, and set the branch into v0.9.1 ```bash cd ~ - git clone -b v0.9.0 https://github.com/4paradigm/OpenMLDB.git + git clone -b v0.9.1 https://github.com/4paradigm/OpenMLDB.git ``` 4. Compile OpenMLDB @@ -150,7 +150,7 @@ The built jar packages are in the `target` path of each submodule. If you want t 1. Downloading the pre-built OpenMLDB Spark distribution: ```bash -wget https://github.com/4paradigm/spark/releases/download/v3.2.1-openmldb0.9.0/spark-3.2.1-bin-openmldbspark.tgz +wget https://github.com/4paradigm/spark/releases/download/v3.2.1-openmldb0.9.1/spark-3.2.1-bin-openmldbspark.tgz ``` Alternatively, you can also download the source code and compile from scratch: @@ -209,7 +209,7 @@ After forking the OpenMLDB repository, you can trigger the `Other OS Build` work - Do not change the `Use workflow from` setting to a specific tag; it can be another branch. - Choose the desired `OS name`, which in this case is `centos6`. -- If you are not compiling the main branch, provide the name of the branch, tag (e.g., v0.9.0), or SHA you want to compile in the `The branch, tag, or SHA to checkout, otherwise use the branch` field. +- If you are not compiling the main branch, provide the name of the branch, tag (e.g., v0.9.1), or SHA you want to compile in the `The branch, tag, or SHA to checkout, otherwise use the branch` field. - The compilation output will be accessible in "runs", as shown in an example [here](https://github.com/4paradigm/OpenMLDB/actions/runs/6044951902). - The workflow will definitely produce the OpenMLDB binary file. - If you don't need the Java or Python SDK, you can configure `java sdk enable` or `python sdk enable` to be "OFF" to save compilation time. diff --git a/docs/en/deploy/install_deploy.md b/docs/en/deploy/install_deploy.md index 6fef8791230..1bff9d29b0f 100644 --- a/docs/en/deploy/install_deploy.md +++ b/docs/en/deploy/install_deploy.md @@ -56,17 +56,17 @@ If your operating system is not mentioned above or if you want to compile from s ### Linux Platform Compatibility Pre-test -Due to the variations among Linux platforms, the distribution package may not be entirely compatible with your machine. Therefore, it's recommended to conduct a preliminary compatibility test. Download the pre-compiled package `openmldb-0.9.0-linux.tar.gz`, and execute: +Due to the variations among Linux platforms, the distribution package may not be entirely compatible with your machine. Therefore, it's recommended to conduct a preliminary compatibility test. Download the pre-compiled package `openmldb-0.9.1-linux.tar.gz`, and execute: ``` -tar -zxvf openmldb-0.9.0-linux.tar.gz -./openmldb-0.9.0-linux/bin/openmldb --version +tar -zxvf openmldb-0.9.1-linux.tar.gz +./openmldb-0.9.1-linux/bin/openmldb --version ``` The result should display the version number of the program, as shown below: ``` -openmldb version 0.9.0-xxxx +openmldb version 0.9.1-xxxx Debug build (NDEBUG not #defined) ``` @@ -181,9 +181,9 @@ DataCollector and SyncTool currently do not support one-click deployment. Please ### Download OpenMLDB ``` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -cd openmldb-0.9.0-linux +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +cd openmldb-0.9.1-linux ``` ### Environment Configuration @@ -192,7 +192,7 @@ The environment variables are defined in `conf/openmldb-env.sh`, as shown in the | Environment Variable | Default Value | Note | | --------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ | -| OPENMLDB_VERSION | 0.9.0 | OpenMLDB version | +| OPENMLDB_VERSION | 0.9.1 | OpenMLDB version | | OPENMLDB_MODE | standalone | standalone or cluster | | OPENMLDB_HOME | root directory of the release folder | openmldb root directory | | SPARK_HOME | $OPENMLDB_HOME/spark | Spark root directory, if the directory does not exist, it will be downloaded automatically.| @@ -365,10 +365,10 @@ Note that at least two TabletServers need to be deployed, otherwise errors may o **1. Download the OpenMLDB deployment package** ``` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-tablet-0.9.0 -cd openmldb-tablet-0.9.0 +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-tablet-0.9.1 +cd openmldb-tablet-0.9.1 ``` **2. Modify the configuration file `conf/tablet.flags`** @@ -431,12 +431,12 @@ For clustered versions, the number of TabletServers must be 2 or more. If there' To start the next TabletServer on a different machine, simply repeat the aforementioned steps on that machine. If starting the next TabletServer on the same machine, ensure it's in a different directory, and do not reuse a directory where the TabletServer is already running. -For instance, you can decompress the package again (avoid using a directory where TabletServer is already running, as files generated after startup may be affected), and name the directory `openmldb-tablet-0.9.0-2`. +For instance, you can decompress the package again (avoid using a directory where TabletServer is already running, as files generated after startup may be affected), and name the directory `openmldb-tablet-0.9.1-2`. ``` -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-tablet-0.9.0-2 -cd openmldb-tablet-0.9.0-2 +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-tablet-0.9.1-2 +cd openmldb-tablet-0.9.1-2 ``` Modify the configuration again and start the TabletServer. Note that if all TabletServers are on the same machine, use different port numbers to avoid the "Fail to listen" error in the log (`logs/tablet.WARNING`). @@ -454,10 +454,10 @@ Please ensure that all TabletServer have been successfully started before deploy **1. Download the OpenMLDB deployment package** ```` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-ns-0.9.0 -cd openmldb-ns-0.9.0 +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-ns-0.9.1 +cd openmldb-ns-0.9.1 ```` **2. Modify the configuration file conf/nameserver.flags** @@ -502,12 +502,12 @@ You can have only one NameServer, but if you need high availability, you can dep To start the next NameServer on another machine, simply repeat the above steps on that machine. If starting the next NameServer on the same machine, ensure it's in a different directory and do not reuse the directory where NameServer has already been started. -For instance, you can decompress the package again (avoid using the directory where NameServer is already running, as files generated after startup may be affected) and name the directory `openmldb-ns-0.9.0-2`. +For instance, you can decompress the package again (avoid using the directory where NameServer is already running, as files generated after startup may be affected) and name the directory `openmldb-ns-0.9.1-2`. ``` -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-ns-0.9.0-2 -cd openmldb-ns-0.9.0-2 +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-ns-0.9.1-2 +cd openmldb-ns-0.9.1-2 ``` Then modify the configuration and start. @@ -548,10 +548,10 @@ Before running APIServer, ensure that the TabletServer and NameServer processes **1. Download the OpenMLDB deployment package** ``` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-apiserver-0.9.0 -cd openmldb-apiserver-0.9.0 +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-apiserver-0.9.1 +cd openmldb-apiserver-0.9.1 ``` **2. Modify the configuration file conf/apiserver.flags** @@ -615,18 +615,18 @@ Download the Spark distribution from the [Spark official website](https://spark. Alternatively, use the OpenMLDB Spark distribution. ```shell -wget https://github.com/4paradigm/spark/releases/download/v3.2.1-openmldb0.9.0/spark-3.2.1-bin-openmldbspark.tgz -# Image address (China):https://www.openmldb.com/download/v0.9.0/spark-3.2.1-bin-openmldbspark.tgz +wget https://github.com/4paradigm/spark/releases/download/v3.2.1-openmldb0.9.1/spark-3.2.1-bin-openmldbspark.tgz +# Image address (China):https://www.openmldb.com/download/v0.9.1/spark-3.2.1-bin-openmldbspark.tgz tar -zxvf spark-3.2.1-bin-openmldbspark.tgz export SPARK_HOME=`pwd`/spark-3.2.1-bin-openmldbspark/ ``` OpenMLDB deployment package: ``` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-taskmanager-0.9.0 -cd openmldb-taskmanager-0.9.0 +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-taskmanager-0.9.1 +cd openmldb-taskmanager-0.9.1 ``` **2. Modify the configuration file conf/taskmanager.properties** diff --git a/docs/en/integration/deploy_integration/OpenMLDB_Byzer_taxi.md b/docs/en/integration/deploy_integration/OpenMLDB_Byzer_taxi.md index d9f9464786d..cda984ed4f5 100644 --- a/docs/en/integration/deploy_integration/OpenMLDB_Byzer_taxi.md +++ b/docs/en/integration/deploy_integration/OpenMLDB_Byzer_taxi.md @@ -13,7 +13,7 @@ This article demonstrates how to use [OpenMLDB](https://github.com/4paradigm/Ope The command is as follows: ``` -docker run --network host -dit --name openmldb -v /mlsql/admin/:/byzermnt 4pdosc/openmldb:0.9.0 bash +docker run --network host -dit --name openmldb -v /mlsql/admin/:/byzermnt 4pdosc/openmldb:0.9.1 bash docker exec -it openmldb bash /work/init.sh echo "create database db1;" | /work/openmldb/bin/openmldb --zk_cluster=127.0.0.1:2181 --zk_root_path=/openmldb --role=sql_client diff --git a/docs/en/integration/deploy_integration/airflow_provider_demo.md b/docs/en/integration/deploy_integration/airflow_provider_demo.md index b8ef81f42b9..39186654e3e 100644 --- a/docs/en/integration/deploy_integration/airflow_provider_demo.md +++ b/docs/en/integration/deploy_integration/airflow_provider_demo.md @@ -36,7 +36,7 @@ For smooth function, we recommend starting OpenMLDB using the docker image and i Since Airflow Web requires an external port for login, the container's port must be exposed. Then map the downloaded file from the previous step to the `/work/airflow/dags` directory. This step is crucial for Airflow to load the DAGs from this folder correctly. ``` -docker run -p 8080:8080 -v `pwd`/airflow_demo_files:/work/airflow_demo_files -it 4pdosc/openmldb:0.9.0 bash +docker run -p 8080:8080 -v `pwd`/airflow_demo_files:/work/airflow_demo_files -it 4pdosc/openmldb:0.9.1 bash ``` #### Download and Install Airflow and Airflow OpenMLDB Provider diff --git a/docs/en/integration/deploy_integration/dolphinscheduler_task_demo.md b/docs/en/integration/deploy_integration/dolphinscheduler_task_demo.md index 4b2d6260b4c..8330f5a3025 100644 --- a/docs/en/integration/deploy_integration/dolphinscheduler_task_demo.md +++ b/docs/en/integration/deploy_integration/dolphinscheduler_task_demo.md @@ -31,7 +31,7 @@ In addition to SQL execution in OpenMLDB, real-time prediction also requires mod The test can be executed on macOS or Linux, and we recommend running this demo within the provided OpenMLDB docker image. In this setup, both OpenMLDB and DolphinScheduler will be launched inside the container, with the port of DolphinScheduler exposed. ``` -docker run -it -p 12345:12345 4pdosc/openmldb:0.9.0 bash +docker run -it -p 12345:12345 4pdosc/openmldb:0.9.1 bash ``` ```{attention} For proper configuration of DolphinScheduler, the tenant should be set up as a user of the operating system, and this user must have sudo permissions. It is advised to download and initiate DolphinScheduler within the OpenMLDB container. Otherwise, please ensure that the user has sudo permissions. diff --git a/docs/en/integration/online_datasources/kafka_connector_demo.md b/docs/en/integration/online_datasources/kafka_connector_demo.md index e5e41531f51..6cc8c79379d 100644 --- a/docs/en/integration/online_datasources/kafka_connector_demo.md +++ b/docs/en/integration/online_datasources/kafka_connector_demo.md @@ -49,7 +49,7 @@ This article will use Docker mode to start OpenMLDB, so there is no need to down We recommend that you bind all three downloaded file packages to the `kafka` directory. Alternatively, you can download the file packages after starting the container. For our demonstration, we assume that the file packages are all in the `/work/kafka` directory. ``` -docker run -it -v `pwd`:/work/kafka 4pdosc/openmldb:0.9.0 bash +docker run -it -v `pwd`:/work/kafka 4pdosc/openmldb:0.9.1 bash ``` ### Note diff --git a/docs/en/integration/online_datasources/pulsar_connector_demo.md b/docs/en/integration/online_datasources/pulsar_connector_demo.md index be53ca53541..42e7b12b789 100644 --- a/docs/en/integration/online_datasources/pulsar_connector_demo.md +++ b/docs/en/integration/online_datasources/pulsar_connector_demo.md @@ -43,7 +43,7 @@ Currently, only the OpenMLDB cluster version can act as the receiver of sinks, a We recommend using the 'host network' mode to run Docker and bind the file directory 'files' where the SQL script is located. ``` -docker run -dit --network host -v `pwd`/files:/work/pulsar_files --name openmldb 4pdosc/openmldb:0.9.0 bash +docker run -dit --network host -v `pwd`/files:/work/pulsar_files --name openmldb 4pdosc/openmldb:0.9.1 bash docker exec -it openmldb bash ``` diff --git a/docs/en/quickstart/openmldb_quickstart.md b/docs/en/quickstart/openmldb_quickstart.md index f43fd2f480f..9a0fd5e2ad7 100644 --- a/docs/en/quickstart/openmldb_quickstart.md +++ b/docs/en/quickstart/openmldb_quickstart.md @@ -18,7 +18,7 @@ This sample program is developed and deployed based on OpenMLDB CLI, so you need Execute the following command in the command line to pull the OpenMLDB image and start the Docker container: ```bash -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` ``` {note} diff --git a/docs/en/quickstart/sdk/java_sdk.md b/docs/en/quickstart/sdk/java_sdk.md index ee698531a6c..83e5c376e7e 100644 --- a/docs/en/quickstart/sdk/java_sdk.md +++ b/docs/en/quickstart/sdk/java_sdk.md @@ -12,12 +12,12 @@ In Java SDK, the default execution mode for JDBC Statements is online, while the com.4paradigm.openmldb openmldb-jdbc - 0.9.0 + 0.9.1 com.4paradigm.openmldb openmldb-native - 0.9.0 + 0.9.1 ``` @@ -29,16 +29,16 @@ In Java SDK, the default execution mode for JDBC Statements is online, while the com.4paradigm.openmldb openmldb-jdbc - 0.9.0 + 0.9.1 com.4paradigm.openmldb openmldb-native - 0.9.0-macos + 0.9.1-macos ``` -Note: Since the openmldb-native package contains the C++ static library compiled for OpenMLDB, it defaults to the Linux static library. For macOS, the version of openmldb-native should be changed to `0.9.0-macos`, while the version of openmldb-jdbc remains unchanged. +Note: Since the openmldb-native package contains the C++ static library compiled for OpenMLDB, it defaults to the Linux static library. For macOS, the version of openmldb-native should be changed to `0.9.1-macos`, while the version of openmldb-jdbc remains unchanged. The macOS version of openmldb-native only supports macOS 12. To run it on macOS 11 or macOS 10.15, the openmldb-native package needs to be compiled from the source code on the corresponding OS. For detailed compilation methods, please refer to [Java SDK](../../deploy/compile.md#Build-java-sdk-with-multi-processes). When using a self-compiled openmldb-native package, it is recommended to install it into your local Maven repository using `mvn install`. After that, you can reference it in your project's pom.xml file. It's not advisable to reference it using `scope=system`. diff --git a/docs/en/reference/ip_tips.md b/docs/en/reference/ip_tips.md index ea42d40dbba..310679140c5 100644 --- a/docs/en/reference/ip_tips.md +++ b/docs/en/reference/ip_tips.md @@ -38,12 +38,12 @@ Expose the port through `-p` when starting the container, and the client can acc The stand-alone version needs to expose the ports of three components (nameserver, tabletserver, apiserver): ``` -docker run -p 6527:6527 -p 9921:9921 -p 8080:8080 -it 4pdosc/openmldb:0.9.0 bash +docker run -p 6527:6527 -p 9921:9921 -p 8080:8080 -it 4pdosc/openmldb:0.9.1 bash ``` The cluster version needs to expose the zk port and the ports of all components: ``` -docker run -p 2181:2181 -p 7527:7527 -p 10921:10921 -p 10922:10922 -p 8080:8080 -p 9902:9902 -it 4pdosc/openmldb:0.9.0 bash +docker run -p 2181:2181 -p 7527:7527 -p 10921:10921 -p 10922:10922 -p 8080:8080 -p 9902:9902 -it 4pdosc/openmldb:0.9.1 bash ``` ```{tip} @@ -57,7 +57,7 @@ If the OpenMLDB service process is distributed, the "port number is occupied" ap #### Host Network Or more conveniently, use host networking without port isolation, for example: ``` -docker run --network host -it 4pdosc/openmldb:0.9.0 bash +docker run --network host -it 4pdosc/openmldb:0.9.1 bash ``` But in this case, it is easy to find that the port is occupied by other processes in the host. If occupancy occurs, change the port number carefully. diff --git a/docs/en/tutorial/standalone_use.md b/docs/en/tutorial/standalone_use.md index e3b87ab8d00..e0d6a704494 100644 --- a/docs/en/tutorial/standalone_use.md +++ b/docs/en/tutorial/standalone_use.md @@ -11,7 +11,7 @@ This article provides a guide on developing and deploying with OpenMLDB CLI. To Execute the following command to fetch the OpenMLDB image and initiate a Docker container: ```bash -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` Upon successful container launch, all subsequent commands in this tutorial will assume execution within the container. diff --git a/docs/en/use_case/JD_recommendation.md b/docs/en/use_case/JD_recommendation.md index af7bc53b43e..02c5a8d2d33 100644 --- a/docs/en/use_case/JD_recommendation.md +++ b/docs/en/use_case/JD_recommendation.md @@ -60,7 +60,7 @@ Pull the OpenMLDB docker image and run. Since the OpenMLDB cluster needs to communicate with other components, we will use the host network straightaway. In this example, we will use downloaded scripts in the docker, therefore we map the `demodir` directory into the docker container. ```bash -docker run -dit --name=openmldb --network=host -v $demodir:/work/oneflow_demo 4pdosc/openmldb:0.9.0 bash +docker run -dit --name=openmldb --network=host -v $demodir:/work/oneflow_demo 4pdosc/openmldb:0.9.1 bash docker exec -it openmldb bash ``` diff --git a/docs/en/use_case/talkingdata_demo.md b/docs/en/use_case/talkingdata_demo.md index 0d0c2102745..50eed548296 100644 --- a/docs/en/use_case/talkingdata_demo.md +++ b/docs/en/use_case/talkingdata_demo.md @@ -13,7 +13,7 @@ It is recommended to run this demo in Docker. Please make sure that OpenMLDB and **Start the OpenMLDB Docker Image** ``` -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` #### 1.1.2 Run Locally diff --git a/docs/en/use_case/taxi_tour_duration_prediction.md b/docs/en/use_case/taxi_tour_duration_prediction.md index a99301e8152..fefb25d40e7 100644 --- a/docs/en/use_case/taxi_tour_duration_prediction.md +++ b/docs/en/use_case/taxi_tour_duration_prediction.md @@ -15,7 +15,7 @@ This article is centered around the development and deployment of OpenMLDB CLI. Execute the following command from the command line to pull the OpenMLDB image and start the Docker container: ```bash -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` This image comes pre-installed with OpenMLDB and encompasses all the scripts, third-party libraries, open-source tools, and training data necessary for this case. diff --git a/docs/zh/blog_post/20240402_OpenmldbVsRedis.md b/docs/zh/blog_post/20240402_OpenmldbVsRedis.md index 2f741015d9e..9ab49a7a001 100644 --- a/docs/zh/blog_post/20240402_OpenmldbVsRedis.md +++ b/docs/zh/blog_post/20240402_OpenmldbVsRedis.md @@ -47,7 +47,7 @@ OpenMLDB 是一款开源的高性能全内存 SQL 数据库,在时序数据存 #### 操作步骤(复现路径) 1. 部署 OpenMLDB 和 Redis:部署可以使用容器化部署或者使用软件包在物理机上直接部署,经过对比,两者无明显差异。下边以容器化部署为例进行举例描述。 - OpenMLDB: - - 镜像:`docker pull 4pdosc/openmldb:0.9.0` + - 镜像:`docker pull 4pdosc/openmldb:0.9.1` - 文档:https://openmldb.ai/docs/zh/main/quickstart/openmldb_quickstart.html - Redis: - 镜像:`docker pull redis:7.2.4` diff --git a/docs/zh/deploy/compile.md b/docs/zh/deploy/compile.md index 2108d507c08..e120275415d 100644 --- a/docs/zh/deploy/compile.md +++ b/docs/zh/deploy/compile.md @@ -4,7 +4,7 @@ 此节介绍在官方编译镜像 [hybridsql](https://hub.docker.com/r/4pdosc/hybridsql) 中编译 OpenMLDB,主要可以用于在容器内试用和开发目的。镜像内置了编译所需要的工具和依赖,因此不需要额外的步骤单独配置它们。关于基于非 docker 的编译使用方式,请参照下面的 [从源码全量编译](#从源码全量编译) 章节。 -对于编译镜像的版本,需要注意拉取的镜像版本和 [OpenMLDB 发布版本](https://github.com/4paradigm/OpenMLDB/releases)保持一致。以下例子演示了在 `hybridsql:0.9.0` 镜像版本上编译 [OpenMLDB v0.9.0](https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0) 的代码,如果要编译最新 `main` 分支的代码,则需要拉取 `hybridsql:latest` 版本镜像。 +对于编译镜像的版本,需要注意拉取的镜像版本和 [OpenMLDB 发布版本](https://github.com/4paradigm/OpenMLDB/releases)保持一致。以下例子演示了在 `hybridsql:0.9.1` 镜像版本上编译 [OpenMLDB v0.9.1](https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.1) 的代码,如果要编译最新 `main` 分支的代码,则需要拉取 `hybridsql:latest` 版本镜像。 1. 下载 docker 镜像 ```bash @@ -16,10 +16,10 @@ docker run -it 4pdosc/hybridsql:0.9 bash ``` -3. 在 docker 容器内, 克隆 OpenMLDB, 并切换分支到 v0.9.0 +3. 在 docker 容器内, 克隆 OpenMLDB, 并切换分支到 v0.9.1 ```bash cd ~ - git clone -b v0.9.0 https://github.com/4paradigm/OpenMLDB.git + git clone -b v0.9.1 https://github.com/4paradigm/OpenMLDB.git ``` 4. 在 docker 容器内编译 OpenMLDB @@ -144,7 +144,7 @@ make SQL_JAVASDK_ENABLE=ON NPROC=4 1. 下载预编译的OpenMLDB Spark发行版。 ```bash -wget https://github.com/4paradigm/spark/releases/download/v3.2.1-openmldb0.9.0/spark-3.2.1-bin-openmldbspark.tgz +wget https://github.com/4paradigm/spark/releases/download/v3.2.1-openmldb0.9.1/spark-3.2.1-bin-openmldbspark.tgz ``` 或者下载源代码并从头开始编译。 @@ -203,7 +203,7 @@ bash steps/centos6_build.sh Fork OpenMLDB仓库后,可以使用在`Actions`中触发workflow `Other OS Build`,编译产出在`Actions`的`Artifacts`中。workflow 配置方式: - 不要更换`Use workflow from`为某个tag,可以是其他分支。 - 选择`os name`为`centos6`。 -- 如果不是编译main分支,在`The branch, tag or SHA to checkout, otherwise use the branch`中填写想要的分支名、Tag(e.g. v0.9.0)或SHA。 +- 如果不是编译main分支,在`The branch, tag or SHA to checkout, otherwise use the branch`中填写想要的分支名、Tag(e.g. v0.9.1)或SHA。 - 编译产出在触发后的runs界面中,参考[成功产出的runs链接](https://github.com/4paradigm/OpenMLDB/actions/runs/6044951902)。 - 一定会产出openmldb binary文件。 - 如果不需要Java或Python SDK,可配置`java sdk enable`或`python sdk enable`为`OFF`,节约编译时间。 diff --git a/docs/zh/deploy/install_deploy.md b/docs/zh/deploy/install_deploy.md index 95d246755b3..8c09f693130 100644 --- a/docs/zh/deploy/install_deploy.md +++ b/docs/zh/deploy/install_deploy.md @@ -50,17 +50,17 @@ strings /lib64/libc.so.6 | grep ^GLIBC_ ### Linux 平台预测试 -由于 Linux 平台的多样性,发布包可能在你的机器上不兼容,请先通过简单的运行测试。比如,下载预编译包 `openmldb-0.9.0-linux.tar.gz` 以后,运行: +由于 Linux 平台的多样性,发布包可能在你的机器上不兼容,请先通过简单的运行测试。比如,下载预编译包 `openmldb-0.9.1-linux.tar.gz` 以后,运行: ``` -tar -zxvf openmldb-0.9.0-linux.tar.gz -./openmldb-0.9.0-linux/bin/openmldb --version +tar -zxvf openmldb-0.9.1-linux.tar.gz +./openmldb-0.9.1-linux/bin/openmldb --version ``` 结果应显示该程序的版本号,类似 ``` -openmldb version 0.9.0-xxxx +openmldb version 0.9.1-xxxx Debug build (NDEBUG not #defined) ``` @@ -175,9 +175,9 @@ DataCollector和SyncTool暂不支持一键部署。请参考手动部署方式 ### 下载OpenMLDB发行版 ``` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -cd openmldb-0.9.0-linux +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +cd openmldb-0.9.1-linux ``` ### 脚本使用逻辑 @@ -192,9 +192,9 @@ cd openmldb-0.9.0-linux | 环境变量 | 默认值 | 定义 | | -------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | -| OPENMLDB_VERSION | 0.9.0 | OpenMLDB版本,主要用于Spark下载,一般不改动。 | +| OPENMLDB_VERSION | 0.9.1 | OpenMLDB版本,主要用于Spark下载,一般不改动。 | | OPENMLDB_MODE | cluster | standalone或者cluster | -| OPENMLDB_HOME | 当前发行版的根目录 | openmldb发行版根目录,不则使用当前根目录,也就是openmldb-0.9.0-linux所在目录。 | +| OPENMLDB_HOME | 当前发行版的根目录 | openmldb发行版根目录,不则使用当前根目录,也就是openmldb-0.9.1-linux所在目录。 | | SPARK_HOME | $OPENMLDB_HOME/spark | Spark发行版根目录,如果该目录不存在,自动从网上下载。**此路径也将成为TaskManager运行机器上的Spark安装目录。** | | RUNNER_EXISTING_SPARK_HOME | | 配置此项,运行TaskManager的机器将使用该Spark环境,将不下载、部署OpenMLDB Spark发行版。 | | OPENMLDB_USE_EXISTING_ZK_CLUSTER | false | 是否使用已经运行的ZooKeeper集群。如果是`true`,将跳过ZooKeeper集群的部署与管理。 | @@ -415,10 +415,10 @@ bash bin/zkCli.sh -server 172.27.128.33:7181 **1. 下载OpenMLDB部署包** ``` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-tablet-0.9.0 -cd openmldb-tablet-0.9.0 +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-tablet-0.9.1 +cd openmldb-tablet-0.9.1 ``` **2. 修改配置文件`conf/tablet.flags`** ```bash @@ -469,12 +469,12 @@ Start tablet success 在另一台机器启动下一个TabletServer只需在该机器上重复以上步骤。如果是在同一个机器上启动下一个TabletServer,请保证是在另一个目录中,不要重复使用已经启动过TabletServer的目录。 -比如,可以再次解压压缩包(不要cp已经启动过TabletServer的目录,启动后的生成文件会造成影响),并命名目录为`openmldb-tablet-0.9.0-2`。 +比如,可以再次解压压缩包(不要cp已经启动过TabletServer的目录,启动后的生成文件会造成影响),并命名目录为`openmldb-tablet-0.9.1-2`。 ``` -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-tablet-0.9.0-2 -cd openmldb-tablet-0.9.0-2 +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-tablet-0.9.1-2 +cd openmldb-tablet-0.9.1-2 ``` 再修改配置并启动。注意,TabletServer如果都在同一台机器上,请使用不同端口号,否则日志(logs/tablet.WARNING)中将会有"Fail to listen"信息。 @@ -488,10 +488,10 @@ cd openmldb-tablet-0.9.0-2 ``` **1. 下载OpenMLDB部署包** ```` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-ns-0.9.0 -cd openmldb-ns-0.9.0 +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-ns-0.9.1 +cd openmldb-ns-0.9.1 ```` **2. 修改配置文件conf/nameserver.flags** ```bash @@ -529,12 +529,12 @@ NameServer 可以只存在一台,如果你需要高可用性,可以部署多 在另一台机器启动下一个 NameServer 只需在该机器上重复以上步骤。如果是在同一个机器上启动下一个 NameServer,请保证是在另一个目录中,不要重复使用已经启动过 namserver 的目录。 -比如,可以再次解压压缩包(不要cp已经启动过 namserver 的目录,启动后的生成文件会造成影响),并命名目录为`openmldb-ns-0.9.0-2`。 +比如,可以再次解压压缩包(不要cp已经启动过 namserver 的目录,启动后的生成文件会造成影响),并命名目录为`openmldb-ns-0.9.1-2`。 ``` -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-ns-0.9.0-2 -cd openmldb-ns-0.9.0-2 +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-ns-0.9.1-2 +cd openmldb-ns-0.9.1-2 ``` 然后再修改配置并启动。 @@ -572,10 +572,10 @@ APIServer负责接收http请求,转发给OpenMLDB集群并返回结果。它 **1. 下载OpenMLDB部署包** ``` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-apiserver-0.9.0 -cd openmldb-apiserver-0.9.0 +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-apiserver-0.9.1 +cd openmldb-apiserver-0.9.1 ``` **2. 修改配置文件conf/apiserver.flags** @@ -637,18 +637,18 @@ Spark发行版: 或者使用 OpenMLDB Spark 发行版。 ```shell -wget https://github.com/4paradigm/spark/releases/download/v3.2.1-openmldb0.9.0/spark-3.2.1-bin-openmldbspark.tgz -# 中国镜像地址:https://www.openmldb.com/download/v0.9.0/spark-3.2.1-bin-openmldbspark.tgz +wget https://github.com/4paradigm/spark/releases/download/v3.2.1-openmldb0.9.1/spark-3.2.1-bin-openmldbspark.tgz +# 中国镜像地址:https://www.openmldb.com/download/v0.9.1/spark-3.2.1-bin-openmldbspark.tgz tar -zxvf spark-3.2.1-bin-openmldbspark.tgz export SPARK_HOME=`pwd`/spark-3.2.1-bin-openmldbspark/ ``` OpenMLDB部署包: ``` -wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.0/openmldb-0.9.0-linux.tar.gz -tar -zxvf openmldb-0.9.0-linux.tar.gz -mv openmldb-0.9.0-linux openmldb-taskmanager-0.9.0 -cd openmldb-taskmanager-0.9.0 +wget https://github.com/4paradigm/OpenMLDB/releases/download/v0.9.1/openmldb-0.9.1-linux.tar.gz +tar -zxvf openmldb-0.9.1-linux.tar.gz +mv openmldb-0.9.1-linux openmldb-taskmanager-0.9.1 +cd openmldb-taskmanager-0.9.1 ``` **2. 修改配置文件conf/taskmanager.properties** diff --git a/docs/zh/integration/deploy_integration/OpenMLDB_Byzer_taxi.md b/docs/zh/integration/deploy_integration/OpenMLDB_Byzer_taxi.md index 0cf5ab7550a..45316c6d24e 100644 --- a/docs/zh/integration/deploy_integration/OpenMLDB_Byzer_taxi.md +++ b/docs/zh/integration/deploy_integration/OpenMLDB_Byzer_taxi.md @@ -13,7 +13,7 @@ 执行命令如下: ``` -docker run --network host -dit --name openmldb -v /mlsql/admin/:/byzermnt 4pdosc/openmldb:0.9.0 bash +docker run --network host -dit --name openmldb -v /mlsql/admin/:/byzermnt 4pdosc/openmldb:0.9.1 bash docker exec -it openmldb bash /work/init.sh echo "create database db1;" | /work/openmldb/bin/openmldb --zk_cluster=127.0.0.1:2181 --zk_root_path=/openmldb --role=sql_client diff --git a/docs/zh/integration/deploy_integration/airflow_provider_demo.md b/docs/zh/integration/deploy_integration/airflow_provider_demo.md index 52490aa4ecc..263823eb171 100644 --- a/docs/zh/integration/deploy_integration/airflow_provider_demo.md +++ b/docs/zh/integration/deploy_integration/airflow_provider_demo.md @@ -35,7 +35,7 @@ ls airflow_demo_files 登录Airflow Web需要对外端口,所以此处暴露容器的端口。并且直接将上一步下载的文件映射到`/work/airflow/dags`,接下来Airflow将加载此文件夹的DAG。 ``` -docker run -p 8080:8080 -v `pwd`/airflow_demo_files:/work/airflow_demo_files -it 4pdosc/openmldb:0.9.0 bash +docker run -p 8080:8080 -v `pwd`/airflow_demo_files:/work/airflow_demo_files -it 4pdosc/openmldb:0.9.1 bash ``` #### 下载安装Airflow与Airflow OpenMLDB Provider diff --git a/docs/zh/integration/deploy_integration/dolphinscheduler_task_demo.md b/docs/zh/integration/deploy_integration/dolphinscheduler_task_demo.md index eb3ecd03d3e..0d010d5c68f 100644 --- a/docs/zh/integration/deploy_integration/dolphinscheduler_task_demo.md +++ b/docs/zh/integration/deploy_integration/dolphinscheduler_task_demo.md @@ -31,7 +31,7 @@ OpenMLDB 希望能达成开发即上线的目标,让开发回归本质,而 测试可以在macOS或Linux上运行,推荐在我们提供的 OpenMLDB 镜像内进行演示测试。我们将在这个容器中启动OpenMLDB和DolphinScheduler,暴露DolphinScheduler的web端口: ``` -docker run -it -p 12345:12345 4pdosc/openmldb:0.9.0 bash +docker run -it -p 12345:12345 4pdosc/openmldb:0.9.1 bash ``` ```{attention} DolphinScheduler 需要配置租户,是操作系统的用户,并且该用户需要有 sudo 权限。所以推荐在 OpenMLDB 容器内下载并启动 DolphinScheduler。否则,请准备有sudo权限的操作系统用户。 diff --git a/docs/zh/integration/online_datasources/kafka_connector_demo.md b/docs/zh/integration/online_datasources/kafka_connector_demo.md index a32ed71cd08..96bac884b71 100644 --- a/docs/zh/integration/online_datasources/kafka_connector_demo.md +++ b/docs/zh/integration/online_datasources/kafka_connector_demo.md @@ -47,7 +47,7 @@ Kafka利用OpenMLDB Kafka Connector导入数据到OpenMLDB集群,其性能将 我们推荐你将下载的三个文件包都绑定到文件目录`kafka`。当然,也可以在启动容器后,再进行文件包的下载。我们假设文件包都在`/work/kafka`目录中。 ``` -docker run -it -v `pwd`:/work/kafka 4pdosc/openmldb:0.9.0 bash +docker run -it -v `pwd`:/work/kafka 4pdosc/openmldb:0.9.1 bash ``` ### 注意事项 diff --git a/docs/zh/integration/online_datasources/pulsar_connector_demo.md b/docs/zh/integration/online_datasources/pulsar_connector_demo.md index c0ebba325b6..4ad1ff1788a 100644 --- a/docs/zh/integration/online_datasources/pulsar_connector_demo.md +++ b/docs/zh/integration/online_datasources/pulsar_connector_demo.md @@ -35,7 +35,7 @@ Apache Pulsar是一个云原生的,分布式消息流平台。它可以作为O ``` 我们更推荐你使用‘host network’模式运行docker,以及绑定文件目录‘files’,sql脚本在该目录中。 ``` -docker run -dit --network host -v `pwd`/files:/work/pulsar_files --name openmldb 4pdosc/openmldb:0.9.0 bash +docker run -dit --network host -v `pwd`/files:/work/pulsar_files --name openmldb 4pdosc/openmldb:0.9.1 bash docker exec -it openmldb bash ``` diff --git a/docs/zh/quickstart/openmldb_quickstart.md b/docs/zh/quickstart/openmldb_quickstart.md index a239a2afed0..fd16b1ba7c3 100644 --- a/docs/zh/quickstart/openmldb_quickstart.md +++ b/docs/zh/quickstart/openmldb_quickstart.md @@ -19,7 +19,7 @@ OpenMLDB 的主要使用场景为作为机器学习的实时特征平台。其 在命令行执行以下命令拉取 OpenMLDB 镜像,并启动 Docker 容器: ```bash -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` ```{note} diff --git a/docs/zh/quickstart/sdk/java_sdk.md b/docs/zh/quickstart/sdk/java_sdk.md index adbe8ed7afd..e6a48b6706c 100644 --- a/docs/zh/quickstart/sdk/java_sdk.md +++ b/docs/zh/quickstart/sdk/java_sdk.md @@ -12,12 +12,12 @@ Java SDK中,JDBC Statement的默认执行模式为在线,SqlClusterExecutor com.4paradigm.openmldb openmldb-jdbc - 0.9.0 + 0.9.1 com.4paradigm.openmldb openmldb-native - 0.9.0 + 0.9.1 ``` @@ -29,16 +29,16 @@ Java SDK中,JDBC Statement的默认执行模式为在线,SqlClusterExecutor com.4paradigm.openmldb openmldb-jdbc - 0.9.0 + 0.9.1 com.4paradigm.openmldb openmldb-native - 0.9.0-macos + 0.9.1-macos ``` -注意:由于 openmldb-native 中包含了 OpenMLDB 编译的 C++ 静态库,默认是 Linux 静态库,macOS 上需将上述 openmldb-native 的 version 改成 `0.9.0-macos`,openmldb-jdbc 的版本保持不变。 +注意:由于 openmldb-native 中包含了 OpenMLDB 编译的 C++ 静态库,默认是 Linux 静态库,macOS 上需将上述 openmldb-native 的 version 改成 `0.9.1-macos`,openmldb-jdbc 的版本保持不变。 openmldb-native 的 macOS 版本只支持 macOS 12,如需在 macOS 11 或 macOS 10.15上运行,需在相应 OS 上源码编译 openmldb-native 包,详细编译方法见[并发编译 Java SDK](https://openmldb.ai/docs/zh/main/deploy/compile.html#java-sdk)。使用自编译的 openmldb-native 包,推荐使用`mvn install`安装到本地仓库,然后在 pom 中引用本地仓库的 openmldb-native 包,不建议用`scope=system`的方式引用。 diff --git a/docs/zh/reference/ip_tips.md b/docs/zh/reference/ip_tips.md index 8be774f38fd..d0e19bbc59d 100644 --- a/docs/zh/reference/ip_tips.md +++ b/docs/zh/reference/ip_tips.md @@ -52,15 +52,15 @@ curl http:///dbs/foo -X POST -d'{"mode":"online", "sql":"show component - 暴露端口,也需要修改apiserver的endpoint改为`0.0.0.0`。这样可以使用127.0.0.1或是公网ip访问到 APIServer。 单机版: ``` - docker run -p 8080:8080 -it 4pdosc/openmldb:0.9.0 bash + docker run -p 8080:8080 -it 4pdosc/openmldb:0.9.1 bash ``` 集群版: ``` - docker run -p 9080:9080 -it 4pdosc/openmldb:0.9.0 bash + docker run -p 9080:9080 -it 4pdosc/openmldb:0.9.1 bash ``` - 使用host网络,可以不用修改endpoint配置。缺点是容易引起端口冲突。 ``` - docker run --network host -it 4pdosc/openmldb:0.9.0 bash + docker run --network host -it 4pdosc/openmldb:0.9.1 bash ``` 如果是跨主机访问容器 onebox 中的 APIServer,可以**任选一种**下面的方式: @@ -126,17 +126,17 @@ cd /work/openmldb/conf/ && ls | grep -v _ | xargs sed -i s/0.0.0.0//g && cd 单机版需要暴露三个组件(nameserver,tabletserver,APIServer)的端口: ``` -docker run -p 6527:6527 -p 9921:9921 -p 8080:8080 -it 4pdosc/openmldb:0.9.0 bash +docker run -p 6527:6527 -p 9921:9921 -p 8080:8080 -it 4pdosc/openmldb:0.9.1 bash ``` 集群版需要暴露zk端口与所有组件的端口: ``` -docker run -p 2181:2181 -p 7527:7527 -p 10921:10921 -p 10922:10922 -p 8080:8080 -p 9902:9902 -it 4pdosc/openmldb:0.9.0 bash +docker run -p 2181:2181 -p 7527:7527 -p 10921:10921 -p 10922:10922 -p 8080:8080 -p 9902:9902 -it 4pdosc/openmldb:0.9.1 bash ``` - 使用host网络,可以不用修改 endpoint 配置。如果有端口冲突,请修改 server 的端口配置。 ``` -docker run --network host -it 4pdosc/openmldb:0.9.0 bash +docker run --network host -it 4pdosc/openmldb:0.9.1 bash ``` 如果是跨主机使用 CLI/SDK 访问问容器onebox,只能通过`--network host`,并更改所有endpoint为公网IP,才能顺利访问。 diff --git a/docs/zh/tutorial/standalone_use.md b/docs/zh/tutorial/standalone_use.md index ea18f1dde8a..50032ef8bd5 100644 --- a/docs/zh/tutorial/standalone_use.md +++ b/docs/zh/tutorial/standalone_use.md @@ -11,7 +11,7 @@ 执行以下命令拉取 OpenMLDB 镜像,并启动 Docker 容器: ```bash -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` 成功启动容器以后,本教程中的后续命令默认均在容器内执行。 diff --git a/docs/zh/use_case/JD_recommendation.md b/docs/zh/use_case/JD_recommendation.md index 8d484ef4c4d..3d22b2fc609 100644 --- a/docs/zh/use_case/JD_recommendation.md +++ b/docs/zh/use_case/JD_recommendation.md @@ -74,7 +74,7 @@ docker pull oneflowinc/oneflow-serving:nightly 由于 OpenMLDB 集群需要和其他组件网络通信,我们直接使用 host 网络。本例将在容器中使用已下载的脚本,所以请将数据脚本所在目录 `demodir` 映射为容器中的目录: ```bash -docker run -dit --name=openmldb --network=host -v $demodir:/work/oneflow_demo 4pdosc/openmldb:0.9.0 bash +docker run -dit --name=openmldb --network=host -v $demodir:/work/oneflow_demo 4pdosc/openmldb:0.9.1 bash docker exec -it openmldb bash ``` diff --git a/docs/zh/use_case/talkingdata_demo.md b/docs/zh/use_case/talkingdata_demo.md index f4ac6bebde5..5cce2584e77 100755 --- a/docs/zh/use_case/talkingdata_demo.md +++ b/docs/zh/use_case/talkingdata_demo.md @@ -16,7 +16,7 @@ **启动 Docker** ``` -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` #### 1.1.2 在本地运行 diff --git a/docs/zh/use_case/taxi_tour_duration_prediction.md b/docs/zh/use_case/taxi_tour_duration_prediction.md index 09b40513c68..1b676fd17c0 100644 --- a/docs/zh/use_case/taxi_tour_duration_prediction.md +++ b/docs/zh/use_case/taxi_tour_duration_prediction.md @@ -15,7 +15,7 @@ 在命令行执行以下命令拉取 OpenMLDB 镜像,并启动 Docker 容器: ```bash -docker run -it 4pdosc/openmldb:0.9.0 bash +docker run -it 4pdosc/openmldb:0.9.1 bash ``` 该镜像预装了OpenMLDB,并预置了本案例所需要的所有脚本、三方库、开源工具以及训练数据。 diff --git a/release/conf/openmldb-env.sh b/release/conf/openmldb-env.sh index b8b1cceb6e2..92dad03700c 100644 --- a/release/conf/openmldb-env.sh +++ b/release/conf/openmldb-env.sh @@ -1,5 +1,5 @@ #! /usr/bin/env bash -export OPENMLDB_VERSION=0.9.0 +export OPENMLDB_VERSION=0.9.1 # openmldb mode: standalone / cluster export OPENMLDB_MODE=${OPENMLDB_MODE:=cluster} # openmldb root path From ca7ab4251188be83b4f1b9b30a44d1dcf390784b Mon Sep 17 00:00:00 2001 From: Siqi Wang Date: Thu, 25 Jul 2024 11:27:04 +0800 Subject: [PATCH 37/41] add blog post (#3936) --- .../20240523_OpenmldbFeatureSignatures.md | 108 ++++++++++++++++++ docs/en/blog_post/index.rst | 4 +- .../20240523_OpenmldbFeatureSignatures.md | 95 +++++++++++++++ docs/zh/blog_post/images/20240523-patent.png | Bin 0 -> 382853 bytes docs/zh/blog_post/index.rst | 4 +- 5 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 docs/en/blog_post/20240523_OpenmldbFeatureSignatures.md create mode 100644 docs/zh/blog_post/20240523_OpenmldbFeatureSignatures.md create mode 100644 docs/zh/blog_post/images/20240523-patent.png diff --git a/docs/en/blog_post/20240523_OpenmldbFeatureSignatures.md b/docs/en/blog_post/20240523_OpenmldbFeatureSignatures.md new file mode 100644 index 00000000000..e55a1f5ee71 --- /dev/null +++ b/docs/en/blog_post/20240523_OpenmldbFeatureSignatures.md @@ -0,0 +1,108 @@ +# Introducing OpenMLDB’s New Feature: Feature Signatures — Enabling Complete Feature Engineering with SQL + +## Background + +Rewinding to 2020, the Feature Engine team of Fourth Paradigm submitted and passed an invention patent titled “[Data Processing Method, Device, Electronic Equipment, and Storage Medium Based on SQL](https://patents.google.com/patent/CN111752967A)”. This patent innovatively combines the SQL data processing language with machine learning feature signatures, greatly expanding the functional boundaries of SQL statements. + +![Screenshot of Patent in Cinese](https://cdn-images-1.medium.com/max/2560/1*V5fQ3koN8HFikmZWJPtykA.png) + +At that time, no SQL database or OLAP engine on the market supported this syntax, and even on Fourth Paradigm’s machine learning platform, the feature signature function could only be implemented using a custom DSL (Domain-Specific Language). + +Finally, in version v0.9.0, OpenMLDB introduced the feature signature function, supporting sample output in formats such as CSV and LIBSVM. This allows direct integration with machine learning training or prediction while ensuring consistency between offline and online environments. + +## Feature Signatures and Label Signatures + +The feature signature function in OpenMLDB is implemented based on a series of OpenMLDB-customized UDFs (User-Defined Functions) on top of standard SQL. Currently, OpenMLDB supports the following signature functions: + +* `continuous(column)`: Indicates that the column is a continuous feature; the column can be of any numerical type. + +* `discrete(column[, bucket_size])`: Indicates that the column is a discrete feature; the column can be of boolean type, integer type, or date and time type. The optional parameter `bucket_size` sets the number of buckets. If `bucket_size` is not specified, the range of values is the entire range of the int64 type. + +* `binary_label(column)`: Indicates that the column is a binary classification label; the column must be of boolean type. + +* `multiclass_label(column)`: Indicates that the column is a multiclass classification label; the column can be of boolean type or integer type. + +* `regression_label(column)`: Indicates that the column is a regression label; the column can be of any numerical type. + +These functions must be used in conjunction with the sample format functions `csv` or `libsvm` and cannot be used independently. `csv` and `libsvm` can accept any number of parameters, and each parameter needs to be specified using functions like `continuous` to determine how to sign it. OpenMLDB handles null and erroneous data appropriately, retaining the maximum amount of sample information. + +## Usage Example + +First, follow the [quick start](https://openmldb.ai/docs/en/main/tutorial/standalone_use.html) guide to get the image and start the OpenMLDB server and client. +```bash +docker run -it 4pdosc/openmldb:0.9.0 bash +/work/init.sh +/work/openmldb/sbin/openmldb-cli.sh +``` + +Create a database and import data in the OpenMLDB client. +```sql +--OpenMLDB CLI +CREATE DATABASE demo_db; +USE demo_db; +CREATE TABLE t1(id string, vendor_id int, pickup_datetime timestamp, dropoff_datetime timestamp, passenger_count int, pickup_longitude double, pickup_latitude double, dropoff_longitude double, dropoff_latitude double, store_and_fwd_flag string, trip_duration int); +SET @@execute_mode='offline'; +LOAD DATA INFILE '/work/taxi-trip/data/taxi_tour_table_train_simple.snappy.parquet' INTO TABLE t1 options(format='parquet', header=true, mode='append'); +``` + +Use the `SHOW JOBS` command to check the task running status. After the task is successfully executed, perform feature engineering and export the training data in CSV format. + +Currently, OpenMLDB does not support overly long column names, so specifying the column name of the sample as `instance` using `SELECT csv(...)` AS instance is necessary. + +```sql +--OpenMLDB CLI +USE demo_db; +SET @@execute_mode='offline'; +WITH t1 as (SELECT trip_duration, + passenger_count, + sum(pickup_latitude) OVER w AS vendor_sum_pl, + count(vendor_id) OVER w AS vendor_cnt, + FROM t1 + WINDOW w AS (PARTITION BY vendor_id ORDER BY pickup_datetime ROWS_RANGE BETWEEN 1d PRECEDING AND CURRENT ROW)) +SELECT csv( + regression_label(trip_duration), + continuous(passenger_count), + continuous(vendor_sum_pl), + continuous(vendor_cnt), + discrete(vendor_cnt DIV 10)) AS instance +FROM t1 INTO OUTFILE '/tmp/feature_data_csv' OPTIONS(format='csv', header=false, quote=''); +``` + +If LIBSVM format training data is needed, simply change `SELECT csv(...)` to `SELECT libsvm(...)`. Note that the `OPTIONS` should still use the CSV format because the exported data only has one column, which already contains the complete LIBSVM format sample. + +Moreover, the `libsvm` function will start numbering continuous features and discrete features with a known number of buckets from 1. Therefore, specifying the number of buckets ensures that the feature encoding ranges of different columns do not conflict. If the number of buckets for discrete features is not specified, there is a small probability of feature signature conflict in some samples. + +```sql +--OpenMLDB CLI +USE demo_db; +SET @@execute_mode='offline'; +WITH t1 as (SELECT trip_duration, + passenger_count, + sum(pickup_latitude) OVER w AS vendor_sum_pl, + count(vendor_id) OVER w AS vendor_cnt, + FROM t1 + WINDOW w AS (PARTITION BY vendor_id ORDER BY pickup_datetime ROWS_RANGE BETWEEN 1d PRECEDING AND CURRENT ROW)) +SELECT libsvm( + regression_label(trip_duration), + continuous(passenger_count), + continuous(vendor_sum_pl), + continuous(vendor_cnt), + discrete(vendor_cnt DIV 10, 100)) AS instance +FROM t1 INTO OUTFILE '/tmp/feature_data_libsvm' OPTIONS(format='csv', header=false, quote=''); +``` + +## Summary + +By combining SQL with machine learning, feature signatures simplify the data processing workflow, making feature engineering more efficient and consistent. This innovation extends the functional boundaries of SQL, supporting the output of various formats of data samples, directly connecting to machine learning training and prediction, improving data processing flexibility and accuracy, and having significant implications for data science and engineering practices. + +OpenMLDB introduces signature functions to further bridge the gap between feature engineering and machine learning frameworks. By uniformly signing samples with OpenMLDB, offline and online consistency can be improved throughout the entire process, reducing maintenance and change costs. In the future, OpenMLDB will add more signature functions, including one-hot encoding and feature crossing, to make the information in sample feature data more easily utilized by machine learning frameworks. + +-------------------------------------------------------------------------------------------------------------- + +**For more information on OpenMLDB:** +* Official website: [https://openmldb.ai/](https://openmldb.ai/) +* GitHub: [https://github.com/4paradigm/OpenMLDB](https://github.com/4paradigm/OpenMLDB) +* Documentation: [https://openmldb.ai/docs/en/](https://openmldb.ai/docs/en/) +* Join us on [**Slack**](https://join.slack.com/t/openmldb/shared_invite/zt-ozu3llie-K~hn9Ss1GZcFW2~K_L5sMg)! + +> _This post is a re-post from [OpenMLDB Blogs](https://openmldb.medium.com/)._ \ No newline at end of file diff --git a/docs/en/blog_post/index.rst b/docs/en/blog_post/index.rst index d3c1097677b..5651599ff1c 100644 --- a/docs/en/blog_post/index.rst +++ b/docs/en/blog_post/index.rst @@ -11,4 +11,6 @@ OpenMLDB Blogs Ultra High-Performance Database OpenM(ysq)LDB: Seamless Compatibility with MySQL Protocol and Multi-Language MySQL Client <20240322_Openmysqldb.md> - Comparative Analysis of Memory Consumption: OpenMLDB vs Redis Test Report <20240402_OpenmldbVsRedis.md> \ No newline at end of file + Comparative Analysis of Memory Consumption: OpenMLDB vs Redis Test Report <20240402_OpenmldbVsRedis.md> + + Introducing OpenMLDB’s New Feature: Feature Signatures — Enabling Complete Feature Engineering with SQL <20240523_OpenmldbFeatureSignatures.md> \ No newline at end of file diff --git a/docs/zh/blog_post/20240523_OpenmldbFeatureSignatures.md b/docs/zh/blog_post/20240523_OpenmldbFeatureSignatures.md new file mode 100644 index 00000000000..8a690290ca6 --- /dev/null +++ b/docs/zh/blog_post/20240523_OpenmldbFeatureSignatures.md @@ -0,0 +1,95 @@ +# OpenMLDB 新功能介绍:特征签名,让 SQL 完成特征工程全流程 + +## 背景 + +时间回溯到2020年,第四范式的特征引擎团队提交并通过了一项发明专利[《基于SQL的数据处理方法、装置、电子设备和 存储介质》](https://patents.google.com/patent/CN111752967A/zh),这项专利创新性地把 SQL 数据处理语言和机器学习的特征签名结合起来,极大拓展了 SQL 语句的功能边界。 + +![patent.png](./images/20240523-patent.png) + +当时市面上还没有任何一种 SQL 数据库或 OLAP 引擎支持这种语法,而第四范式的机器学习平台上也只能用自定义的 DSL 领域描述语言来实现特征签名功能。 + +终于在 v0.9.0 版本迭代后, OpenMLDB 新增了特征签名功能,支持输出为 CSV、LIBSVM 等格式的样本,可以直接对接机器学习的训练或预估,同时保障了离线和在线的一致性。 + +## 特征签名和标签签名 + +OpenMLDB 的特征签名功能是在标准 SQL 的基础上,基于一系列 OpenMLDB 定制的 UDF 实现的,目前OpenMLDB支持以下几种签名函数: + +- `continuous(column)` 表示 column 是一个连续特征,column 可以是任意数值类型。 +- `discrete(column[, bucket_size])` 表示 column 是一个离散特征,column 可以是 bool 类型,整数类型,日期与时间类型。 `bucket_size` 是可选参数,用于设置分桶数量,在没有指定 `bucket_size` 时,值域是 int64 类型的全部取值范围。 +- `binary_label(column)` 表示 column 是一个二分类标签, column 必须是 bool 类型。 +- `multiclass_label(column)` 表示 column 是多分类标签, column 可以是 bool 类型或整数类型。 +- `regression_label(column)` 表示 column 是回归标签, column 可以是任意数值类型。 + +这些函数必须配合样本格式函数 csv 或 libsvm 使用,而不能单独使用。csv 和 libsvm可以接收任意数量的参数,每个参数都需要经过 continuous 等函数来确定如何签名。OpenMLDB 会合理处理空数据和错误数据,保留最大的样本信息量。 + +## 使用示例 +首先参照[快速入门](https://openmldb.ai/docs/zh/main/tutorial/standalone_use.html)获取镜像并启动 OpenMLDB 服务端和客户端。 + +```bash +docker run -it 4pdosc/openmldb:0.9.0 bash +/work/init.sh +/work/openmldb/sbin/openmldb-cli.sh +``` + +在 OpenMLDB 客户端中创建数据库并导入数据。 + +```sql +--OpenMLDB CLI +CREATE DATABASE demo_db; +USE demo_db; +CREATE TABLE t1(id string, vendor_id int, pickup_datetime timestamp, dropoff_datetime timestamp, passenger_count int, pickup_longitude double, pickup_latitude double, dropoff_longitude double, dropoff_latitude double, store_and_fwd_flag string, trip_duration int); +SET @@execute_mode='offline'; +LOAD DATA INFILE '/work/taxi-trip/data/taxi_tour_table_train_simple.snappy.parquet' INTO TABLE t1 options(format='parquet', header=true, mode='append'); +``` + +使用命令 `SHOW JOBS` 查看任务运行状态,等待任务运行成功后,进行特征工程并导出 CSV 格式的训练数据。 + +当前版本的 OpenMLDB 不支持过长的列名,所以通过 `SELECT csv(...) AS instance` 指定样本的列名是必要的。 + +```sql +--OpenMLDB CLI +USE demo_db; +SET @@execute_mode='offline'; +WITH t1 as (SELECT trip_duration, + passenger_count, + sum(pickup_latitude) OVER w AS vendor_sum_pl, + count(vendor_id) OVER w AS vendor_cnt, + FROM t1 + WINDOW w AS (PARTITION BY vendor_id ORDER BY pickup_datetime ROWS_RANGE BETWEEN 1d PRECEDING AND CURRENT ROW)) +SELECT csv( + regression_label(trip_duration), + continuous(passenger_count), + continuous(vendor_sum_pl), + continuous(vendor_cnt), + discrete(vendor_cnt DIV 10)) AS instance +FROM t1 INTO OUTFILE '/tmp/feature_data_csv' OPTIONS(format='csv', header=false, quote=''); +``` + +如果需要 LIBSVM 格式的训练数据,仅需要将 `SELECT csv(...)` 改为 `SELECT libsvm(...)` 函数,需要注意的是 OPTIONS 中仍然使用 csv 格式,因为导出的数据实际上只有一列,而这一列已经包含了完整的 libsvm 格式的样本。 + +此外 libsvm 函数会从 1 开始对连续特征和已知分桶数量的离散特征进行编号,因此在指定分桶数量后,可以保证不同列对应的特征编码范围没有冲突。如果不指定离散特征的分桶数量,一些样本的特征签名会有小概率发生冲突。 + +```sql +--OpenMLDB CLI +USE demo_db; +SET @@execute_mode='offline'; +WITH t1 as (SELECT trip_duration, + passenger_count, + sum(pickup_latitude) OVER w AS vendor_sum_pl, + count(vendor_id) OVER w AS vendor_cnt, + FROM t1 + WINDOW w AS (PARTITION BY vendor_id ORDER BY pickup_datetime ROWS_RANGE BETWEEN 1d PRECEDING AND CURRENT ROW)) +SELECT libsvm( + regression_label(trip_duration), + continuous(passenger_count), + continuous(vendor_sum_pl), + continuous(vendor_cnt), + discrete(vendor_cnt DIV 10, 100)) AS instance +FROM t1 INTO OUTFILE '/tmp/feature_data_libsvm' OPTIONS(format='csv', header=false, quote=''); +``` + +## 总结 +特征签名通过将 SQL 与机器学习相结合,简化了数据处理流程,使得特征工程更加高效和一致。这一创新扩展了 SQL 的功能边界,支持输出多种格式的数据样本,直接对接机器学习训练和预测,提高了数据处理的灵活性和精度,对数据科学和工程实践具有重要意义。 + +OpenMLDB 引入签名功能进一步缩小了特征工程和机器学习框架的距离,通过 OpenMLDB 统一签名样本,可以进一步提高全流程的离线在线一致性,降低维护变更成本。后续 OpenMLDB 将添加更多的签名函数,包括 onehot 编码以及特征交叉等,使样本特征数据中的信息更容易被机器学习框架充分利用。 + diff --git a/docs/zh/blog_post/images/20240523-patent.png b/docs/zh/blog_post/images/20240523-patent.png new file mode 100644 index 0000000000000000000000000000000000000000..ad64beaee918be4b8b7e5db1ed08cf1245254c5f GIT binary patch literal 382853 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

eAsxE$(Dc# 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 z Ecl*_!nDG`nY=+&hF#KK8|IVHG zOr`Pm_H|RBP#;)286@IKccZ0N_pF|b&Ft!B$TxYbVs0m@O7E=s_Oo%cf>>nxb()QJ zmie4B3bXXP8r1hcL;`RMcA8cM{%oJV3GM}3LYFXTY;^N}5o zm)h#A97j@3sM3ZBtH#!QSE&baCGOwdb5Z>ldwKl2kyiojfrNI2f_S`q@_A&`g6`Ge zS8~(F$PmQal$39Lm)-yS-TgV_Nqq%GNPK}Lu;}8Rq;}hNTH`nJnM@2wjw&`)4UD~! z)5P}azp-=9!MG(nr#uyS=isBmiZ3@h1fORp?!5#?nH4b5Mf`|MgOSj$ymaF!ez4?p zsroT3C}T0Il_jBfyCu^M7+~|@$wPAVk_g_=Yv7kqXoUTCd5m8MZ9Cqn$@KCJR0@B6 zoW1_G1(t%5tiRL*Qb^E{Z_moVeR089fBm<(Elx zICQ~3oLMH?T4uOG>Zxh}gui)?csO!N@T$@09;1cyqIrhqR-YUO>_c(b#fE3MD0b(l z)<@;U9Kt+(>}9xd3EQx*3kv;rdg>wmY$0&4vCP~k0}!9r+rfQz^{Vn7UPtDTaJPw@ zw#^Os-R&p>3052Iz4DrL`#+xY`)VAAUi6`YH)|7X zs%en>mLXH{S=3CYZ2I4QKSKO3rTv ze|~YPxxVJ_E79o>`qzP5RB~3o1`Q=Mvsdb}_jr5WUR)UI zBm?T(IAXU>yBmHn12Lw`FH_)}vE8RZtr%^y;s^aNvJ zhb{SIlDcKrWUVX;>g}9AEU_m~c-Ur{_{fUb^tTjwC90*O6Sa znvvH57CWSZ*)&3-wZVlaJbzufZQou!(OdcOaL|{e#4S;zys~o>d?CK8-uEbidniqG z-+2?i$m394VoK?U6RMU-aXj(*UJ0-M%{1Ozj zhxp}747iJ5k4PM=yTsPe;Ro|+!SvOI?!{~(Qk)|uq1zB?m|BpN#q1F=3iLr#-S+l1 z>3G?YDD8GBftZkE<7Q5@Y2wtzUM0SA0M$<{Cl9NPlGlr#bx4be)Pgy=`&RI#QDUOm zNFfnf|K!L|h$2EkwY<}p_*94NeK$_DuDA7bpO3ySm8S1L z9~oVzb3BiD$-{@X)|qZtpBN7_XNb4z-zET1B0!Z09RY!I|C%$?!~;G>?tTJOCOuxM%swv7o~ZeOUdfLqBmkQy6-369%&q znviqXO)N};1U6Bj8xV}ktG1p$D%FOjsgCc+)GMmTzAH1Vl8Fc#AFhsGTS_C=)N%p0 z(JWB?X&jdFO{hz!EW_A=NN!|F7^EPDAYRb33|Me87ptFMn;7+{aP4$qn zF`KA!uVPA*L46MCGXKCxA#tFRv&9(PeQ&0A+T#|+HI~bErJhokO5mNuEg`&W;W*8@ zld(o~{{ddQ4C%v5UCUTc=Ereva&B65@n@8vY#~z93zibbB4f>aBMjsk-G$7=xPaLG zCq|;X0v+nc8Ac&!eu8Y-5+xFt(f~aW_;%K$Eze@WV!~VZ=YQu))lIzNt6`h?ldo-W=g=6n7;hKu6+LTI|_SAn4+%AW4#6qny zsP}sIlgj}bZKJU}4s7GyL59@-&bs`#!Tu93JRzqrJW^b?>Hqf)e(DbFSvjO zkX+Uspa267>tQAnJ5h83MVT^9qj8^#aA6aLAE`gGFYo9E+U9*z|H%Y203M-0!RL21 z!MPZLVP*_xGKiYg2m1~Te?Ce#t}H&QaIweMAZZRlhmg-@#UW1(USYRt&=}`ol465u zb;mi4$EYCCi{a{9_Vn~iQ*hqrmU0hXP7LN+>IQ;E%}4f5j@E=0H_64yhp+vnConB)= z&J7nuY#c7>`zG0Y3AQ4i7XmtvO+X|=qy?GzT7M+RtD=WfrCO{jtQMFQY2>MQ=M300 zh{yfQI;_yI^2&=I^^~8U-2YYx;_o7Xu+(~ki9Oi z#KH1e218et{6UI$<7o~oYHEo7-?o<38GMfrFu02|y#2R_MTx){LKHM*p;?+|%3lJi zSm%zm%-r}iYG3L}g-aEzTsu?t-AH}YKIJQ5d{fBYALbp6_q>m$`z38qoJo`w8K8hP z%R4?R6fRByQCt$Qv!Xz3u%`m-1ol z|Ab><3@cF>QS8VkSW8Sbh$vbcs!TpcgXUx5+nd3Y7gw%HcOJfw_jqf2n;V^irx5ff zMXbt2wgTt_g@2{%e{LE>@s4(3%{SPU7;fS!xuM_ zAfc~5K*)wf$w^(k>QN8-zY((Cro8~#m2H9i34T%gZ;b|0-S)pn z)-FlI%5++|G9mw%SDxS$9=cy!c2umGFGp@NN7V9()UEqR4HZ1GG60NPtzULLYu z5WpmXzp8c07Ef*X%r!7Xq}d9nJZINk^WVV3rEsK)B8cX{Jy}tE0Y{NQSE6w=Qv`|N zww;H$G$L>e(||rU^4i9uqvzojDyVyWPstUrI=%v~J~q^UR0|#tz#G~duHC!=kmJMz zjDQ3I^rO$oRW}+16+=>(qH;Te@QDBXNxKPMf7B#Y>WkI!N*AlMPs*;gKTOYSG}wQf z!f*yTHAsn6&KY(48LQC{XNIwCL-tUA5Jg1+)Ckb>`r&NtnwktLG`GuI(eqUD$iIV0 z0D%F>Mmlm1-;wf-B!sbkVA+n4gT@qfa@dOd$_z&ARRW1B=V;W#qIaNS|1mSkXp7v? zy4+GN@_eg|B91VU;(D5z=k%|s|m~V4W*BV z-fxd*R2-rfw32E0GeFMoquLze7SQ8f7|~_Fur=R_-2m}LyZN3Q_x8N_C*f4M7J+Lx zp+B-OZJi@PGGU4MZ`qg3-JJU^lkIC6PrHyv&!Nlv=Z$~zdKCL78f|TMEvshL^HL3i z_j@r=&_MD?oX^85?6Wvo=?qxZ?#wv!#+Xo%LrQ;NZf$yII&ewu=3pld<@=t-y&5@n z4~~NaY3X;{d_q75fuuU2dryuBEj?~|Ch#C3h_;MT{oRGsy`ENjXU-|}+L4(MnX(`( zpPIwR*`ccb0iT>Vfi*he(PPazbZRVPyrX4*Ih>-aCFXU;CQIL*Xz>J&-oNJfhSc@Z zTMgUWjju`=_uux+={sD(cTk>!1%z3`iC4|m_S|Yf@GzDI)$7ulVmZ3e)+O*ouEK;( z`Hg8$2-k>Qj5T!OYAh>AU4pUkR28FmW2mB>&E{f|7Il{q2&3|pn~-qaT72Ak$`i>f zCX{^$>ZO7pk)K0Jk`ejINP?#V)AR9E+6tV%QjOpK?2WfRMU2322o?;5G5t2hRcb$h zL%g-m{i~@XyLcF~NJ%!GeWFeO`mK(cPrBad+MEm&u@uwJTk$r2!z~13oo=a zn7zxhKG3>}T6}AEaL}N7KQK=4lqiR0f;@XVZ&7THvZg;Ub3Aq)1D~FE@VC)blY*5{ z#Cqp%Q${9Jx{{QG81=uYd+mu9i0`hvB6V%-u_%52-zzw(|Bh>pYYE&H0WAMvQ;Chc zz5T}=*Y;;NPF%q8jl?k?%aG$!O|vrpu)#_D(r3Po9u6iH+1tAZ92Yd3FHNmksvS{G z-ubHG1g;%D%X4XDp^Rh(WF@M1patb zaCbB0_h3Y-n3!cBs%MMpi+na$gGYc&l%aabMX8|8+j>?dWl;`CSdeKX6j2G& z9!a>N@Sifx&eO@YW#I_=8bwuO(Rg$*KLWu(`}7bYs?l&Wym*S1>>t3M(B^8*I;py6 zg9s!z%o~TzYf9E_^Alb|RPe*Y$-R5S&6La_m&6XI`YSQR9Z`{4cCaPMGz)Jld#wX>wGmNG^{)J^ONQ zR<6n}EkrmLf{GfL##&I|`C|nDP)-b2!w`4;=}@6jjUP-6+_Ae1CDHnop~Ni)YwX6I z9o>3Vh1w+D>l1nBqZAm2KsX}L;C?*|N|L$`3B0@r76DFSUeu}WU9EG_Q zHx@RWH)Lh3`pf-ooWVQ=K|*f!WM-7JSYq3hB`8#87Ai8LkKtBEVy5M9QYwx3!%9)09i;{rZKHC~4^@uEq zEs?myCNG)|&6`)_&sP5fKT~tR)-;WyDF7TMb}8U}Bh>ss#jHh%BO(Jp1_}cB$U53_ z(gQ$t3C~XKAu9smP@N2;IuLWfK9=sF6B7Un=l4^to;KwxqEO=DQ+^zt>@zi`;BTQ) z!S3!6UIbFDHv$>#^`Dyd7{%A>BZ|Ql5`X?k{8O*VsbJd{*-xp!;!r9tK}h!HoN2c(cCI5 zq6mqIYxw8|ntsv;9?a`ZQY;7_m(TRD=+*&KtV77=MDB1_oe^zuVS(}ADZwdn-L;Vs z{(>v3GoLs|)cLs84>nH@iQkTEJjrnVPK2_HE{!gok0%i}oo`|oA_|FszWso{o=~*V5?dS;A*{;au~c zk^7D+ovO9k432{HsRLsdJF_ul`ua4*$v1q4BaLwcVeMmzSh(*g)kJK5e%Qy#yE4n> zLUp#@rr@Nj=wbt1C;Db7QekloJMtt^w(tLraY^={-(1YER|tAkfAAtG5Z(_4kX)lw zk_YBf;77jXc-^Tdir8VNNPlgq`sTJaIuXx0?51;^JdU!C_U7DSb@aSFeBHCfIC@L4 zC?E`~hnf$i|HQn1`9toWI1uWDz`|f7fPiWwSLkq+1avSHfPKhy-nx z`Gd&8*aGoaH*2tF^W*Ri`>F zVL%3l)n2T3MCS@XrA}23KTi>3|J5KLo7t5$;bU99S^CjlM-AP^einH|+moSa05d`Q zUECQN332IHlz5|3l}-JvQ_kgi&euUae{y)k-fx4bFM@A-_l;EVx8=RyOm4%O*q(cU z^UMPH3y;9V!TUk4Lig&{%PvdXQtO5LH?IB?D2U+ha+X?|dEI=d0U!>9!AdBRf;WvB zzn{E#Vd(nX3^3p+6#Pz&&Z#epp5)|DYOd4lmeMl?N|szM!SqMR^52NC#UkrJ5&&-M*gGZsgFd4N^mG*@~+1kV(qlvNae zUs@njj-IS#@)R;2uYhr|V{7WW(0XzpMToxghoT&RUB1;$AjqTq9~>3ts@WQhhFVZ>{AHc5a-W78PRX! z2<_lj+4%~3j8KF|yKma~TF_5p+@Zhsz8~k;CSAtAF9`lRj$LN$0s+W8idjFx3H$X2 ziD#7-kN<2vnJuT7BmqwBC7EYe6Y39qijRx<>E->}kmC}q__3p6 z-~2GG?x@;OI=RM#3|<(ARGWf+9d{j!9B+#yQ(*i{rcZAVJpmpWw&J*>h#|@G4EbkO zE6)MGF;&5Bi|b4#c?2UuG){#B11gieka%U9To^nRoM%&KB%6Wn5P3+Dn8vaSy(W)` z76giZVGs0Mt;?zs2h0UVg%*)75tSHx?>%MyZ)3C26-hmip9gUBy}!6I;vsOJ!4zwe zu`6zp2J-UppL}?pZ2VGhlFq%<$|=szC&1V3tD(*4pufhhW_oC!l8dhJyxUVqEv1FrS#LDH>+-F}x3WEl3_u5`j5+BSOYnH8 z{9f-KAS)$RdbycmP%H$aot8tKe>02NfQ6!!@Y><>=OKunjMrsu_0cSwoi7_r5naJJ zAanMf=H7yq!^n|NRLm%6L^hZ-_e4=#Rmw)2Gg5LPkxF{B+jQB*w#h78(GV4Cfx3dP z&cp~Hk1F1MyX@b2YwPW;H9PWux%KaQI>eRsib|#qQPTWnU1^c+HqIb;{LSItoX^kW zbUD!uooA0cxEpJ;kE&k~e`ihtf&sZC_jLyC=&TuXWlk9>Q}dh|uru&f&J)3`_%Tmm z=5=jL;?iipjWXM$t27Bc(_BW>_2sX4(1Lj44(eRI+@jcezCEf`ugf%v;ozZ|6`Jx= z!3M&67LtV-*UoDJx64PQuFY{ACBnNYM55{KbB2^b(EMgrv}7vG$mW*rmevNS4@X=X zg28OSEeia~c`Qb*F^+|h2x`pG62yw=nRvgYu}>?SjC$LlnQUmh!dug9WP&0BBuXE> z2=~b8!Y$9n8Qkw(`tUU;TbwKZJk^2eZ)JQT_-#_|-L0!<&0@+Zlw?t?yQ_4h&41!d z7=%1BmZyKo3l-u_()%kXkx&k` zxd)KrP&*M6sHz?KHQ_{~B0ztF0)k&)AUveM->-X&kG({<_`CD}M%w=i-rZR?^jrM> zmFvir<)Y_dH{H_vHQxVH+5hMB;Q8Bw`_gx6VI6{=A|5`TO-(GW`<47z0ZE3Zy*)#n zw4C~f6cR=?06jne3k?R=Dif8$>HOxbo8Ozd^qc8{#Bd$B>;t8glm~=&zL3v+5Hb@Y z28$DfZxk-`Y76Gkx8!>7A0r zqP8N%MgDH5c*}wvG=#I?*35DaIA^bDCz}P7^A6-MgvfeQDX9D^blz6MM-jV-$lUE zAFex=4$GF#HktxukK&ttx6Gj^P;uKix*}#BDYM^KMW>`X#yT*9kX5ItFsHjtYgxRQ z5VKWM*wPWHx9o>8m!_~u5QKuPqn7I0q!ez#P<*(}UE~+s#_X%--qe4ItY5tSnakpadnIrFwPN@_>5g$Gmn_}ZV}G4=ykU?^lVmeieDm7L|?#se^%(hN2f!0lwI{FdwCwf{}swTtr7?< zqM}4&`*b!XKcJL$|Frg!6bgYg( zM{}ucQyBvTKnMpxTUkT7Ur{}q*s&*pE5!vdjW^S8%KLt2r2b3N((^6)10(#RY9GNl zC%=w%KYm*lld*nE^Yy6Q$5iWXo!VfgALB6DQ{^Bb=_Cd>c^q+okY$r87ucj&LMOjY zo@oP%a+Nc7*%@~>1e$nTBYFl$=vPrq%MuXY7cWEwrwupqOe4qJcPDG<@!y`E&cpZR z2HvsVAPIcvZusWLEdRZw&d|=_^=N51*toZ8!(NRDB(mNt+cMu4zC}!YzC4Af1q=(< zz@Y@`FDfi-j*Li1+Q-MyUKfh0O<|#55BH+G!mhe_Gk^F>cB`7;Jaw8z(0^h0{=mX- z)0t|xoCIFw-`vg7%@Sv4p9+t}sarFg-jz`)UA31>cH*A${fxsv*Pl4KF=yNC?rr$p zDxG@~KTMhsE}a(}PfodZ{bv;ekHBnE4BMP~Fi3@YHFx9AjI|%m#0s&WDtB&bdY3Z| z;c<=tYR)>6yN|oaFU8BH$OOW&&LGi|GgU7fa(?&qQMaj__PU-9KYvqNiH_-rZ&Hds z5qAydJ%Gg|T$CuDGYZ)3bjeqpp-UH&PUZAJvs7NPZylgJ%DvT{=1y}uFQJuHDW1zv zJdiyduaAIsUn9j0bp-3{>*k$c%JF9T31{gvMp(3DB_&`fCvvh*cba)6)p+(i)L+{4 zSflg(DD3T*mL8A-4vj*sc3GF*(+6pcUiaVbfo#oZhaCTO;}l7SA#vtvEa7Nf5H)6y zQsUy?lV(~8GKJX3EtYUQv=D|4o%Y6#>Irtn(eAmN;iM?C89u2Ko<1C`=1P? zt*?_gFfYKR$Mfs8;A{N*p#P))`}zAj#`_EHUi^F0eXl~W^Z+($Dl!#guXbUKDRmA{ za+HBLPqc2wf{8>`s`U@ds47;$Z+Ca+aDbtzgjsx4ZRam7ayCuf!q`V-NU(UfNdn*x zUd2rcHnnw{4y^(dMrA?$K8FoRW{&~HEN(DrEFZs)y}teo$Tfcvc9?^4ow7x z69(zVE_v|uxSm5%4~y2fys*JAcN<;`lO-!G__5)OR)SJI2WP+ApAViXP*@B%q3I;p z?#>=w0pvP1Zt@%W3X-Gck|@e74*GNQA9{RZYx7}eQRR5P%OR59I)q+Cl5GzcUl%jw z386`S_;__+={RFdmJzA1Y$F2K>mLlCuX57$zth~w2K1}81q&)P;V&uR2|!?JCe4RSef-SrEgQ0I?XNu{PYbzu6;D` zLdrY`2F)laMapK0aJ2k6s!RjAcN6FyXSb->@#P;@Hc+351UuN-K0)}-^2uikVChSX zFRjYGBP~*#M)twk2Py0`ydyshYHgTm7i~3bCa@RuW;|Qex_ex9F`XtcM~cYFBgo`A zpHz2*Nw(8-@0l1AAr3MKFgVl=>!%_>WC|&9O$R;W9F(94sA?vb1289o`?~+RSNr4> zDCV-|c$q>(Y^cl_P|xFdsR#2mUQPxFq>|QoiTY7zvtggU-%cJGk`T(Uua`ol@Tk)n zjreqno$X3- z%!Xh`@7!iVj^E=!ub3s<18&FV@p0o@5o{-dLzeko`yYCLn&@RR5m0~rio5Ioj_Num z70+dF9UJ}E1lQZUUauAX1c+u3-6b?OPVi5gHdnRJ<xRzLXFfgKdPyP(#a3pV|jA*%4dmhkk1>1uvuo^Ek3@ zxn@)1a&1xWJBS9#;+Z`dJ-6@ji%2P_s<#*ecNiS;0Da`2U;{zS_6c-A7L*QyZy#XM zqbtTVC2)o$YT1tEM%~LP(;YUtuhVx7>rVA%8*gUYdaM?+K7aSk8ogP6(I6}b8!kTh zZBa6!Zj!A8*Sx(P^6sMk$2Wq4(t`vowb0mE3FLl%tE(hb>3@>UWz=Ne^Legj>Fx4; z*YI}$c{VT;X!?7$tn2h*Z_eD_WWQ;SAI#bEnbUu_O(BYv$THt`tVy4$(nFOsSgdg- zh}}0OTnP0Mmg5XLjXga*Q)rv}P!@-THP3k_Rm(ya*-QCo7t((9sEBc#33<9Ug-8j+0{&D#34v)>mtJ056emH*{+=?oKH?uz~6Zs`kj0no`R*5PP%f;^cXld_*woge-1W-o*lguz=R!#bKan_GoE#%q#+_N~~&+mU$%d*SF zd_+9H>Kc<8u5InIizi2Si?jaEkK_GcJ2zX=jyrd+TiLmLk@1aie&X%7=)htId1gKK zkntmj+g@4?UTXk?1DL4k0*RTK=y4n;W)WCnKrD;w`k8tFA)f|>z(om#bd7;w2>|9_ z3_`t=NM2~r2aw?JW7P~ZlE>V3yWUK{=RF$|ew~VI!%N^7B$tr%G6NJqXbwjNc`#Uk zvVj8j_Dg%bAUflvV!cvpH6>NVy~h}iOOjf41PUna4ndkACCaDgdpdab9{s+z`+0hZ#N9eK-}g$p zR@ZY#w)DRnRl&CR6?8l;^gkb`1Ag4BXrM3Y61g_@>m`s;DjFoM^OhyQ`!$>I z-FdgIZm?gnwVWtmf>Lnt1F?%6@Ra~aKlZgIo9)ba@C{gPgk$UmRNqS9RCKpPhp#`n z&z{9eW&toLFIm$-qb}=pLvS=fe>4^*h7Xymz6eXdC&?guHHZiuSEMgRwM=HX-H)ai9P~$FqMm=#jTkz)b z>^({NaYx<3H`gh`T)~O!DN0XHufxIm*7#K2k$BS{ZPA^W)HVFJ{~+Jr&%^3(9~}C> zGd+-9H0M7DxDT8^h2TDP`Mz9ZZtEBKy!@dy-g63XQEXO=tvc=~byohkm%CVqziS58 zKKSHnM^=FuyYGP-JAZ2ONpTT%3`Sk$)r6zJ(blG@RwC(0=aY64welK82m9nVFOwL3ybw-5)0oL%C>d)2~f?pjH* zn7aj0Po9Uw20caFs%`LRub!TjH@c^Cm!mhy;v&r_MDIv>V<7y1Gd{u!r|uXIrM=G% z3~D00;no#jq1ea2p)McCi%uLILW7Mr<|V@PFn$8I!}<)MsX#%ibIC zob8K$Z9Lec$Z6J<_}0mMwK&EPF$$t_o#a4OWXvf?bznsN*$hq|{;7Z`ViukRSQHI% z@W0i$%{^Yiuunet@DN8PXQTTZi?87N;0;q|UPe1_jCC5}&gsq>zIah{b#hs|`(un* zi*+l&Ys={SE??8S1xv1f0C;@idj@#SuGX&D$V`Mtk{Tab{0>3)U&r*oCZ6f%i+jhn z^S?Q;I^JK#bY(nR^8u=5wUxd*{is+jyGUT3fIzg{yH>e#PQtGVgjNNCh=ePQ9*USo zWIy7%nZ)7F0h`u@C3Zw0{?NiT^Id+t&33BaZ#VWwJ;vFcYadq!JNO4#KTI~m8yIPo z93zficJ+1E$pU?^K`ZC_54pt166pG^IdBk0)~+#*4C#?1VR|V$^2#DveRVZpX;eu> z&<{~pagDXLbqN{?)l4UuuMn&)Z}%pD0PHq^!_KlB88-~4;4Ir>(;E}WO45MNRRa`r z9v$oXig4l9*UUN3&-I@P!C8-2ujubXxS#9r;k_80a`pzKajz8G-VIOq+6s5 z50KZ8Y!yCD%}&I%wECkSD(qR3{@yuZ9)ep>gxNO~G zC0>iUb5Aqwj4SQzR7GjUT{(Lo`hAYBa{8#59pt;-QwSXw=-}Lg@AP5i=$TTYA(&JJ zCwAIz$N}V6&Ayx}GwXwX)BoJy5i?Y^+#UH^3^Ivntgx z=54J>rD4ZTGhdIh%lb%u-VJlHRDZl4-ub2Ti(PX!+7g&R5%@C7F4Zt&ap7O`8ZcH)qjKW!AKXI{ zz?H!MZvxQDbD+v%HHC^`GTDB)^%&m^my%8vMMCSds}W3MD)8W|MJR|e8iiWzv#VaR z@ZH9KY|>YJ#A^1c8dyM8n0=aj-d<{b7t(JxmBkZU93RVY)&IY9(BCP;#(-pouu`}e zYkLUE#=eN)!@~f3(wDGtK(JkH1xsbkd82Kud12r9)7L76_#OG?*>f=oz1VMX*AB_y zZ<{5*9dvN;GUVv>IV8J09etAk{;L%E#*TM6RXxSSLo#1^w$Qn;@Qs)@cP2%)Gysbg zZSV>#j00<3AQeS=YrAYgzTjcFNi$FwvAd?D?#IDQ$An}#1+QzshW>4;^=+gdfJVqoAGdChIMWo&zVH+tP zEG~4UBaJ(da1jj_?cW#DmCYtp0g@LV&RyQ%Q{(V}8Uw`j*z;mWC|yMftuvlbj}5J& zM$*5rH0X~u*v0MYFK={wg?ibwB)^$?3@NsVp{K7=7f)t}Zcbi{8O`((hKeoFQ)9Bd`s8l{Efq5Oe9&CZ_m zx2^Wl$sO}#t4ijjBi--36A%wYvf_aY!t6D%5eyG6JzI+OP?l#P5h39KzW129R+!3> zmpu=D-p112>Sl-Q_o_f4*#fQ>dJao_%?hr75}jKg8o?A&pw!XHU%jb6dd!yTC;Li7 z=)u3m3RURa+S-)m+T!d|smcV5SR2 z0Y=dCYGNb8rD6?KLLh%tRbi3)^9%uuupGWrAkvp8dU3AE0wm1-mPLxX#adKk zOyTA{yvTlhsPv)4q4SsrU6eqLQHGD-GBb=h+8MH{Mu#I}t)-4D$}#R&$>EQrtjGtC zOdWST^$Usnf*Am7h3)-oQ)a>5sUm5j3{5>*y#3Q^{|G@^jMmh7yNIpmYRxjrj1nS=k+!G#T3`*=mU$0q z$%VRIb{BjM57EQGnp`_K9e~9M$1Y@)c2_!vwWT7}smVfv>}RI1MNr&z29h?j88q9>wCpA>K}bw^VtWoB;AQ?@ zt)?W`Y1sv)>Q)C^;VMqcP3F__D0YKb;w(az+Q0rPjSoQKWoOm)%NM0N4H8kdQ3(|~ zh0c}sz>pw)wKW|A#+Y5I;_{lcoWGvbb*7Is#w6IkF^vwC*{#1NPqZIK z%0!)7PG96I>#20Lca9zk~$s@{60gia7tdeo~UU4uQbHo zckUP6``sd0A z>UxRp3R!XGZI2VOo6o^!(s_$q)x%%=&pW` zM`Z$Oq&Bk64dBkU;njF0|KbW!z4aERNgR*D(n_w6b+L9)R;o2$!5n-ITnogu@)Ajp z)_IgN^z=}$Z=w_0GJXrJH9mJ>Rq-B;a}|*YLvum4Lh0w)EzT?gdx477DpK>xYThza zXVULQ+ac^{LU>huxO$P8x>-7mp%cfI@`+lU_-GpJgo;C<`b_OItyP+_Ib4P23{KNc zU()%WGI+yAj6kd*$b7~L!c+=+v!UE-I9!-gL6j!yIiTIj&F1v->C;*{VhVJXR&HsW zsL;gUK$Jl(&NP;TR2tsjSo5(*Qr1JxCsuSibZLMRm+X#Jl&vOL7YL3dP!xeqj5~aA zx7`V!Iw71@s!*iXX>!SK_KNtg#}H}|YgOYWk1M{(dfb7R#*8>L>LvB<8`;N8y)|mj zs_iHdb;4YHbF7%YDD>v8D$CBejlX||mp<@$a!9(=vzIAGd<9a>B7=hTcnz};$m_*i zC{s}+2Po6r-);40U3k}#Gd0HP(9%?v9tL<(1AmTLZvL;J=!`{#u{YX6fCUDBaUQsl zW-kJMN;B6+!_h6Erz+(ypJkBOep*0z3Mn=FD1LZLB~|=_m-_%$eY5?rw&=O{`JW z7DDhjQ2Xb#d?dbX`yZoKMsJg7x(kst>Ghv^czMOTi~D634nZB}Sn?7ZG_={17E@@& z#r0Usgu{sgQtnng8Z~OS3?Yuk{}P6zl({zlQbFU62~yOkBfgXsrI!SR5WoW>E-&G9 zFeY@c_neprqh~S&%ehvoh|~w?3z6XU=WB@(@vG|LQ4v+C6O@3?i;yrW^hmw5=;2Se z<;2orgU+1lV?1%m6Qxp9{d6XtWi!^S_=6~v!Q`6Lh?IdsUFsG?jy!*jM++4Y(nea8 zi_PG%qe7=HPw-hTYE2}QQ{b6v<<#t^?;>R7`~KxfQCC|dY((H-e&{zbi$ngTi4=ut zge>V!6au9|#n94*u@6yCk6TFW76sA;uS#@;|F`@_-1OxHUid|wa&K%!pq!a^i3gGk z;WyzoqD5Z_q!TF^3hpe^_fTyU-UHWd1W>K+%dj_QMWuF}r1YOmr-&*V4ALc>~2!TCP9V0ub4>Bw9@^U9{Jms15QKp(0W0zMuVX z!sRE^4u{OCvafDrEWbXSQxt$>$Bd6)NYuFWBpCY8;v(4)*>H=AQNpbXNz6&dkPh@<|F~zGGvJk{JC(-XZYzaWNf$wX0T2f2 zsCTj|hhQN~e&YB7BoAn;*4b6)QBx9$0^w4c$Om-u{{2s9^NNw&>dnz8!U?K8;K-_+!d{heZ)H5CcBSQ$*yCcwdHx7aaq^Fxv>=xO@k5`=){gY__6JbY|N zY<0wmW(uuW8dUgB4MZ5D#XDB&+?-}hKuZ!~-W7t}TF0VrNLA9BoRS5T$>RN?lF$cA zploW<>56D+pbwY(c2e-#nob*GH`DqrRIW&9~}c;pL@ zJdAMB1ehj1OKKD8AtC1R0-Pacs+5(u)77Dd4UwL{5p7Tb1zXKeKrqGZWtjbbRJ$X6 zO_U6KS_tz{##fZji44=J>t~hMu1w7kSG#{g%6krhNM^Q~Jmf}cB zN3cR5H{W1EzoOQ{@IKQuQ7NZdU>S)>H7J?jhGmr4rmkDI9GmBIzi_>=RXm$w$27d_ zdmzP55#1eqcu@_8QZa5mJx+_68l8t*fwo`K?Z5PgovsN}kFnR(u4^ooWi(<9x^&g@ zqPqg%(T0jT?p2k#Q$atu>Ua)2k30Wh@#PV^lhEP-JL4qjk;4ZY!Ra!?iHXXycWUY| zI3|dnsk1y3#>|_Bof9chfJPwf=-~|V;;%X_DhNO!H2DRP2L)EFPMW!fe0No6-yQ-w z3>uvp+|kdU33M`XcJbw-Sn}hJtT<2{ZX5b>81tmFKst-{uo>m*mp0vM>tPjPtPt#m2p~LZaMakf78PjU@=5GvNIP1C6r%9a0C@wv)5peo_rFLI81K#O> z7qd{p)38F09`~PO~-g=}=2RGlLwUiG06IFZVPB^oRP zA1{lGZv%}rKc}=i76~#pR_BE!AnR_Bb!c)iwH}BXxgkL5AtWXiT7-u$H~fRm+#MQiZ54Fd!W3Q6j;wi(Ly)+F)0!8T#v?4W_SQ2px_~ zyVEL>p3Qvy4G3snDv(6eAghj{`fb6>JW-@KG90971gOKpF&QL@jJs>Sb8fd?*Wtv; zUR-Lin)y>eh)+yF#l2Tqp{joYuM=4JFH9#0&BW{0AnyX$7pWJe#ThWDG#Z812Q*1$ zV+af59Q;YGd<8MU8eTH1{+cEFNwmz4#zcCMRpRo;>DsBhev@t|c#`yl41W#MY&M=0 zSEle?3{YTRn?y2p{0qKT)%VyVv>!|IK}$0OOQu_TI`V6FyT~R5H%mjRh`O@bXbW;- zL@>JRH}Zo801tzR@ZIc%Y#;A5M7O?m!{}nZvUeZ;+S3I7*WBE# zmyM#Y%D&F**I1qBQJH)06|%t0)svg^-0wdoRrztUH!~!Fr{uHfcx%}DMS>?=|3ti< zcfB_;-ouyNg1aZNwm4(ovrV3iKVZbaygnX$wp{_mj(nUtH)ydgB?o>$@9HfS4!Gz4 zC*eAnw#<=vQngn-(0?KIF6H-QYz!P1)SA|+HX9NP7J@kjUSnjVf__J96qv;?kUttH z>mPxRoB{#^;BcUuzO*^Ru(QSl?xZ+y`_Hhfm@xR`#H;R8i!&DvN@Nr{FsMYT_3-8% z-R$ZsHMuEbQ^YQT0Tz}10umCU{Cdo=>ER8$MW!gmmtU?MCJh<0d)K9~lWhG)3A;R# zjV{={qtfXuCen-zXh@i51oj2~j+9Uyzo~k+&fMg$(o?3>P#-@dN2QN@mknx*Khf4f z;%xY6PrS!&nAFDhw+o|m6-Gw5N%ExMAi+RscybtB~| zCu?&=BI`~^)|`UJCub4`vRya?i%y}E#_1kq=0$CAHBJNm zr=#cb&{9k~?f2vFvP?dm5L7zykCBy)3>VO>pJaFn!l&$g=re zKeR7EW0$T{UhXi=PcwhC!s+d5q#B2oGD*p{MRKc(7z!Q7K9NeS2x}z%RjQ*`bOr<_ z5W<7Tl#q+Zwtp57DrFDICn{y^KA4ndFj}~ZeoCSGyH6vqQ}FJ|-?h6^2kywSbl;p9zI8nLo*$p(9e5hFR3X70t>cV1XfL~l8S|cz zM8vRi^b{X>;_rS{;7NAq#bnCdk9D00ciT=_D7QF6nCkj-Q+asj&co&VHUH@kpWCNF z^lbFb_G6NN_cQkoX&EbdV;2gS9o>@{TlVntH1W06q=Co# zu4aGX8B zFEV?5+SNTsWbQ)3eiFoSi1d9v^rW@xupu5HB_*v(VQA{GxTK8NL!-ZlpuOs^ChY@|dHv}V3-YjAng z*zuV8Hx%(TyJc^n4lz>_vg|TSBe6qjwam1PljX@acI@RkM(opB?TJzZZC-JFbz@(! zVJGRR;LRC8LVP-Wm?Fd$^aViUv`VHQi6|YKQ<-}$$WcQD?FsYh6|V`lcLo3;&C-G;bNC7z zdb3R(zC_11`5>_teZT|*iU{KI_j8xjrDbt0qJsq_$&U+>_pyP4fO2PYcB*=-2IfdHGP*NlR937Y_qGBip%JZoY@%vVofJ7} zi1sf$%BO$A!)YCdde+J!bSzFul*UsLK|q*o9#Ro})X`v>dN)%vm!Cz!A7>4woNMT! z(JbnRroI@(9eC!9Q6RcaA6=Cv?pvV_NU4LPtD>gt0*GZU5oJz5+jDskcf zDMR!|oA!0U`;Ze##HgSU%)CXmX!Dg^%aW%FERNd>=cDb}N?b4RdcJO)cOQS4(;Tz4lX3tYg zx}o2r?$*mlqdAf=kj(30r}OM-<9fEYqUz}AzWe+-|6LD4dac{@eCF-ll+}ijrcMq|jH(#E@K8&0FDK5`- zThg{%M!k%=0YMs{{+){G9bY z<6zVP=by2?6X^TD#AbZx(2_&=xt zcQ|HW9y^X5wA$=_!5?3LhAX^IwI5tS&Z7^ONk~Y#Pp;k>{-O##j>mU&cD~9!y!%~^ zm)CVX9XHk}33dTXlc)&F9v|Od{sWPRS#~7|qfehqH+*~lfPwM;Pg3yTSza3vbH^k_ zGm(97HN}^&el3IaC;=LkE3G6bp%%`+zNn!->*B+~1()aEv4o0T`ujcY?#A&mV9Zbh{;KmSKrv)Y>Y4{KlEDeg5j zB3pA_S!GW+YM?AZ2(@|&b$bRQG!ILxByw!1DZ+fMRAuf{9#(?9iB}#iiU})z08o-o4SDJZwPoY(1`V8G=1Y${K@mZ09|RC z-FOjJ!lFJurHCN*E$Ir`uU)_DY8zsjlCZ<|)}wFNYTxd9?)wb@aIC?s6&syYCshE{ zk_0N4ubrl8m{PcYpm9vIYAI=<6D5&U?YY|fVrc@g_GfyOw z%%s=S9>PuANn-uV$zdC4`iKI|SD^jlYT1w43G^cSPJUKTTy+(|iW4d5!H#6qXli@h zEy&ujlQcj`sg3tvlvWm&(K-nQ(56;C6)V(kW5$KJBD&v2D9wG!Z>UiJe$dT5BvXm? zrfm2Q`@yRm&%oz0uk)N9C))kxt`3(`<7A53tt@2$%`%Jsk(xJ7G~?Rc&P+Q_g7!;W z`Kbp~9`N;AZu;-kwotl(*JGJ{wnNt&-ZkD-I7otWXHC-j+2k#sLw8fSUVlE%y2b0P zJzu}`uD`BM8=9Y&J#FB*x9I==eEoUD!^2jzyeCj@Db8P5+ zx7usw!VBmlf&Qf=7N11()xAlm5{>Vs)~4?8_`LJ6jEDen%YAFjW#-kE3QN1g?9%4D z!^4Z`L1*1{rJ}FOLi6=Z@Ag^exqgGm5=vZjS$3C0o-v8wEvyYN9{5eO8|Ek1%DA0PYv?4NV{g31D2 z7!tI=9d~cf`5s4=f{=Q*xuVd8vbpbFPe=Fu7lREA4QFR(hooo}t&_)-0sDN?o%KV(aMmZ_@D1n!@nMQPp=$5sDltzh%Aajc_u*Atz`7 z&6!-bCC3#ozaArt|Cy*uLB5Zt&Rh-HWu} zW0~(U7ikdv*Ebrr&AshRi(dYmAb<~)+U&cZDY$k2?9Io|U;b>JKImZC4*-~&np)=i z+SB29{lmD2-tv~$u^n`zW^ftP@?Gw<+g{G)dw-lg$@W}`i!0CYKGz^3Z7M>)@IT|x z72s2dYm^D*11W_7wcQs9#u=?*x|>WI#^P%5X!m9G5e|6afKBYzfPCrnK(xw+xene0 z$*M(bFe$c{Fi1=}O(xW&QYA&5H-FMnm3NR6P|#|QCq3tMqD1xAle9&MIW-&&jR9&I z`>^ou_>>86qMjY&mz(NMb{yzRD6byvHb1D9oMaRY27-;NX4`59_pyw%Xh&0Y; z*~KOF#Dkmq%0x)x*hYh!FpmAl-40_pW2p#1_)*%Y6v_x3d?im@MB48Q{J)Mn6ofup zr~x@JO9pcCcI`-ovaDSa7RhM@-~R<)`W+w~y)4)u8VP%F!l0$jO;h>+qw zv39kz==vQc536rd>c93qTx#$2KI@L@@Dg1DrIRrbjEO&t@FZ7j?KLWw@s&hr{=Bjq zciXb)Z$0&)KS=b_65!)ibpqodORx@}+(c|;bFAooINQ4)gjOc?U$y6~ZVt=eIoa3} zrKo(kitCPp$cISrJ{{L2PWScrlwa)_4l(>}NDYfS&8#GSpUQ zo*ahdx|nIVj*X37RTU;u_!Y)_ceHW-u(_yW$FX9Du3~pZR8-g7+dH}xi=w2Z<+dMb zsS$io?eCkEFujj9C`FaU6PHbqQA)=SF#7f_=Ac~*%zf?WtKqgQ|9Q**olHT%^H$OC zrFdp0&X)VR{T*_i=k4!5fIRAd)|79|s-X_m@x?m$Jtv5w-^C($QdAs-5 z0)HfX-5r!9^&CoL(iOPq#}K^#z2((<1J?O*fw#QX=luJ2`Sni-cHHx%;k(ySC0YBU zzr*9Na)HOc$%-3p$F=3YH^psRcQ$zkz1O+tzOO`EUC<&)QM(D6_xygff~}b6@dvrL z^STW@3SH*$>N?-TwqDQN^PhiP=6mjELLYE%yui^|FgoO7(e*fkVDMr8{dLQ^-Dksf zInU{d*2~*l(cn7JaLemC(=v}Bezb9w7;y31Wwz%eIz6jQ+cHTA6hfQlbJc#3$G>~C z;C~tYK6?FoINfI_jL6==`yEaICJeON>U+DmMTdg|?dfybx$YtC+}zxj3d^rH=kioZ zY355y$hRNj*6q7}DN~;g75#i>cr9aNV)DB7U)#3)MlA(g6l7(ikG;@>$-MsUbo#lw zLt6qh1`f~M_vaF9Mabb0b~(n!tI*>3FFh?S=F?e=Tw#5zpn(>fLH+2JA6$LQ13)BogjA9(KW(uehrn>l|@StP4&d%4og=C<>C zEImjb|BI`s_t<^@zFkoq*Y$eG?{{0ROXm3(Bmeot@;{s4x!?UWnZWHYEy&V#-!mK` zhQ~V`Ov#_;=Eb}o$uAM)Vi_L4oN@wpU^E>m6C?8GT!wQF632&Hz2UqS0(_17SOdge zhe;4x;^iTv>2QrDGV+%{><@{!9l$o`M=`WnORAlve{sJl-QiJhDuT6ALcHD-Vf$4}R12w@Uko3X-u} z?Gdn03Y=ssOQj2KmC78cz`!8WcKim|QPU7z9o>DFjM+=#(vSn%7ACNTuI~CrW`ky< zxu7%84X9`^(Ml{uj4xE;*PA>1xfE#aFud`&DOtO)jjS6r(QA zp|17bA!XCx~sRVhXr?xvK(nPOgMz zzWA_kTJ(!=_QL<;=&IwQc%0}_3W9<(iiEVJ(k&p;-QC^Y9FhXk-O|zx(j|F>w8U}r z(cN+P+wX5b{CC{3-0r@4^JeDFU8H-U>5N^Luh9`C7XHC^pgHIAv!}h<2X29n33<{n z#nWhH%w}q7>gBpHV9gWRa7k4K6PPwlJXJc+xZf|aWZf-6j0U=M_ z;NfpKX&h&h)$aS}rF``SbIRxm7z_)(!~PG&05X>wcu$sBfb;6ryRP$LCI5{dO4*1J zi8RVC*SUl|(UV7gHeAr{RaFbCu4@l`vVASxEKbyaJAqm2-CczdOu%hU@%wYo@7)b{ za!Qa*aaFzR1piL2NJJ@52x7;4OxhVCpmGR{9wrS^M1{DE?XZX-gWJ7 z$aVcZ@CD9i#RvQT;Q{Si&xoCf^VLGP3R@bkp#PkXRbJJBKxnN#TOL5)AX?;sTPi~= zMs?@+ZyntYNI=dHN8S9VKSS7Cx2Dxr!TVADn$`Lqv%Fn*4n}FaF!sTJ^_4@E;6#fI zIkv9ahKBy;BW)H>0Bl%y0q(Vz(TL?GB!*|`yH{+a@AcHk{aYzlVMb_Um}Wxsq=P0_ zd)vo+wZms*rwHzY)0bHigA>=x#Y^dRI29UC1WBWG?wJM_h(Wf|RfMi2DDNM^2!_dv z(D;5WisK|kP!G@%U9EM+%}`^>pQ`G+AE9{leW9TkIP24Ti#Xx?(-7QhgL-$F;)1)x z+fTq1m=k~pFuWGunaFAPzsnC3&+qQW1##rQ86suZssB?Zkw~jk?Q&}tC%khuMESIi z+dXul-+30oL|0;=c!WLcy<~(qUjSYnY(CmM39`uq5c5q87cVbKw4@4PgKDZXanaG+ zOD&VQP^qJUX=7(+;B#wIEZf@BQX>**T&CW3US6T6r`O&N-)oEYd_l6|#Z)|d5`pa8 z)_Tk9_y|NHrl^90Hq88~y1F3>KhvoKNjpWurxh6D`leg7`@#x>ya001Dc~wlc+A#C zQ271>GG;Y^)pvI`6uMpG2Af|S|PCIwG|f!i(cPr?wn&upFD z!sp9A}E z<@=r7y`$dRPBSb?HawfFQLmVT8+oti=)UEujWeU11F6Zrg#1z{iBb6t4~jP-O(5}V zs13qvYtx$0w1-sh6EVFHyR9|wcp(ooCI)xmaH&?6DGdy%!eOE+V<_EqoblG$pq^zM)*L2@fYG`Yrb;E7m)g|8gX<5 zNAHfttH$xfAgX>ClN+^|KY;Xv@B+tQC_ZNh4ux#Mh&(hBdU7e5Y;!*G&I!7y))ml9NN809csI1 z`%z=sy-ImJtf-b&_9-O5;F}NqtW@LrM?ZPl5psqlm3I3y>IesO&R1{bVr*zM0>E>PFZ|W?i~@!;Wn_VLRo_)lqV$`eFXd+7^!b#vW z7vjCX|91eCTE$;0EF6||8xEi9R72gl3?LS+_=y*-tY&i`)lpSdLh*{u%U)*jC}aQr zD+w~~QI9P7P%gX>NCPb^UQVeDXJt|KspO!i^T9tKHEbQX#Xwg@+xp})vBsY=DUIPTeeF~!#ykT_GaZwFCXA^M&hg_lYn*xAi_w_A-Tq=vm=8u`Yz~V1#eT8=;aJaauQ+Wyhtq1VJcYC0PU}O8yMS4yWzOp3IiOLA8t)}? zDkA^n*%u zD18zxdOsCM?sxcc&Xzk3piToz;(Z=~P=CIB^4sic|AtkkV;Av2*>Tz@M->Xsb@Oa0 zPm?Xyb83GIB~awsPUemiIv)a7;UeFGSL!_K_wOFK@B9PJy=fDq$1-NNvk25=mG}_1 z40--zldr~WD=q$`un`mimvwnhhw#!$9nNB!Dg)ibqCCV_cWKp-kI!RTXNk>Nt}wRZK$3;geShzi(@-w@|Fk{7t1IJwb{rUD_lXC_Fq|JGFHu^F%*j zZb4XFvr5>MZ?99}rXB#DV^*SW z$G{)`^ltT2jtm;;>({SZdH%8=xTlt;&le|iAM$Z?blK9d2ZHmm$I_@1G#-&J()(kmm!MuaW~)EEsQ%CIsrm?;Icytc#DSe)iYGP6Y$yv+~6;FsoillKQOkN zqGdR4&S^KF-Qfsr>qb6>IRzeMo&?-vB5!Y$paGMeW@d$yPycLyn5=JP1PdcE+~Aw= zfd>TI+aCg2@xNM=O*e1=Plo)Vy;B_C8%m{@aX<7u7mJfs#qa_zvh+fFsl*wHqshY8 z=OZjwi0vE|MXoMqfWNnLt9^p|PYR2J+WdXev_g)>zwl4yN+1+>eJT9n*qsTeR2^r& ziF76@;izwMS(S~-!N$uh-b-~J57rFM_JHQ&y>JW6j^DVJQ#>&?FxNA;W^3a$<@=1TnG)`#)shQbePvMMv|MRh2dTYMKk`vl!(IPY%R^Q zcZ?m_C@-JavJb85KUw8>gS)$X&L%m(*GWjDs3oLU7nBp@APx24HIDO6#$N_Zaklxw zH!;FZzR3LeLlZBq@~et4YeS)c%7uLBloYQgI;ma-L=SBQ(ItbV8JO5@D@kE=%ChwJ zWv2Px@q%$^Lz64EBke!zejy$=S}Etrid_`^3;)q}1a*krD+*V$jaW*wH}MJI{3lN> z{M)2L7S$nIFXBS)q1n-9)5;@)*qK_>9x(WC%SZc6HYPu59eZ$^-BWk+S$gi7WkFXF z%^!Ldyycz_MGn|pkVRTenY!3q?XD2 z_41dLo33JoEhaUjP8k$2z+u+VY(+qjzNz`lQhs^&-F{UvGxXiXrfsm89y-;2Z+Du6KnYHZ7kDav!h+sc%B5-*{0wWOu23FcY#tN%80Sm;$$oW9D7>wyN#e*%Wme!;@zq&| zefT$=tTpn5EBVQiRP1-VBVqb=`mA1F9&Ty`d+j2`*EY8mZsNm0LHn?6Vx&Szx6OTI zx>(N8d#`xG2pq6CU0mBmu3Kx|+j+C{E?Uya|ExF;d9a5obh@UYWxym?uTqr@UWIpG zjn^!Qc(RVeZUuNpa3R}sYn6Z0=uM`Kk@;F1RSvr6nLTj*E)Hr<|Flo4I%+MN`dDzMi>Er zO6qXD#}AZMX14y+ABxw-%hUT9roS2(tG8EEOJe6K*Xswgjn{H%Ec_4Ill~9O3y=2; z-QuJQC*K8Q(GIc@1Eyzk$g6GDvdcYA1x&L6Z!=&4jwAmHN=`g_V zqMc^Bay>i}3HB9*emn20deGwr$pPJ1E#^vJ6k zaE11VfTO4y+svKcY(JB#tF~1$ENs1|R_7^Rg#8z1vgIlw!vklM6TOeM$WB)(*KVK z^f~3D1S6q-lk4`zKs0_cR}aC{{E+frARN!-K1kJbbab)UxeX}q|CTXiJ3j*VSck{KJiptH5b(-#a_==Dn4JQ{m#~M2hge{@^s?g<5oisN zol)SB|`&wg3fLKwkM?&3U9Y6=sYyvb89)U<$$Jmo^9x=AW=Y!`l~t zB4zfRU2vC%62VcN3~#-@zLsr0SL|AUdDssiaxGFk95vsXab6kqQrqwKkMfVTO-geT{;?PK3st_w zUG@B?SPuSrQg$MFuup$!^uCpAymO0ySn4o4j%}`o z>&K@e>oDAiCv0=(yhl&?a3Jyu@^~b)rZV4NXQtB@VG=7B6w4%rx;7oAMr!?RjBJ~2 z;*2FJ(58c!N=rrLu~?^9}PvtOZ2#BWEUa zYLtTmYn8tglfi5}v0ZdC7aet|VX88LcP;}4t%i!!pcEs$Py7IXI{S72EGD&NzKWmy z7doo^@Y_=T_-}-%#wb+#c*{lf;px5d!d+Zf`nYrRSIbOpghij|c;@nAJte78eHf&| z%XI2LDG<1+kPwJ5K9`vzuwbHQm?qe}Z;KBPF&Xk4n%PXIV|=!pnI*+rTyI&zo!Dq^ zCnU_KT@Q3h%ja6%`Zo-`vefd%tXwQhK(*6hz4#E=YXHWuq>= z{6i@4MDYJEEbQzCQ*q~-Y7e>`rtVmRt+xIUDBkWVfxBF0=xaMqgsR@Qy0WM-5azWMvTi z%C*_E7;;hMJM&h-GgvSP1dM0c^{%(nz_O*B9;FI3orU8_{fU!fPx57}^qF5n^V8^(o;#!7Hu;S1_KA*;9AL;*uY+8}Cdq^X#`Yh>F(2nWo~~gluVcQ5 zT#bOUJpU~MG*C@RG~iFU1{4a|?;CX92Z%oHA%*UD&GHbJ88gq0@cLrj@@A9}y@&+V z?Hd$OiHS3Li_HxEf`?W&z&D{S*MJ-@iwUi_fRI^bQ(*oAKOMN`9@yw!^+mJgCvUm> zEO0@uXETVZWc3<>fABQcoE|l~WD@rb-21X@0pf5UAbcv)d;$)k1 zbA~Nm`{{XuF)i729=Skx9mS;o1Xm5vkW!z&4q#MS$7%nJYAjfNbHeqtsxi?4j- z{z(eO8)}Le438=*^ik;_bE0G z6nix0m#T_Aqc&tEK?2K`K=ph0^mKSoh5OCVdO?+pU3pI)20fny3F;niJj~CQTRUIm z1d_r(U=B%8Y>HzRtp6(bAS=$5ANwJGom6bYVOdNJ6C1CsX`5vd3ouy!j!{*vmO{6f zcx0O2c-b>RSCRgM0p2tr#a6G-o?%xPOrM&s^=rzW5tWYG1t)F9%2n@znl3wgnaj=e zW02t&AFW1OndD?1NZok`Y5`+tAE|MV+Hg;z7U3Ckaupo3=rMPLIQ`JRFf*B9sLnE? zz{AsQuE)d1*XrZMra%0WRGycK_7D1RELMHY@k>GubWFMqtSdwLCKNOKC>OE8dJQXD z1yMKO<<810@XleznKf4;-SWTsMp0i1zX<`qnoduCzmt=DZk4D$^EO^A`vzl*i`ev| zajrw(uhJ4Yk3DGUw-y`exzE1PDJ$1f-MX1pb0iWZl3RcKTlDvfYBx*Ct!?K%dUHCS z|6En-4%thaO2;5YXa#GBIUWxS!8C5qCPReC=6Cf!UfmC!p6$X$A3E2Qg>=7t z^(vO6^A;R%+fTX9XNc!r|Ni-w;OZT&(c?70X8Yw}7vR-*=qSKGEpsuvl$x%5W`cK6vO0iezz!Xus}zvPH0g!ar(x~ zN(Ns;l4)Wx>DV4ce7M0HSpuVy#EM%vMCPnkV}|9LJxL-4d7U}N&kJff4>m{ z{xJ+pl?LfVOd>W)7CLt<*xRWyq1rlD2MnT}7Yp58H$=Q$kChEPY;dx>`Ic@4CMbBV z=L&o(6*TQxTF3~*ktak9Z$wUp3gKc)k;O=E#;$OXBETdhTS zrPYn_XjRQ8e7vb{@G4ft5{KP%adbXM8|45McI6NeBy`Pea)nW}+FL`Ky0@J-_!`Zv@3X6zFBBA4XZ_ z8V5Q$w}el8TB?C^CNNp&2A8<{dbCtg6KfVUiKl3T^0>v`J3z7aCRdC3VwZxU^w_mv zoI5&&vhy*MZbFl_^~3AOQHxx2jK?HhZy&wf12WABp}lj~Ne+W$>9uEU9s!30vY1Eo zqbI~4_SrsTXt*xJf z_xZg{2ue8NG*K3i#f4%{I|4S}3hXN|rf_H#=mnan_Ad2&-6Xm~i;nGhL$VxJ87eGE zrg)id#&)TCU4yLlogQM9n6Wi>`NQayzX(TgEY#Whb0|*Qagkd}hsj%! zEB&?oa<&H9x$0+so(GA>aEgkeyuYZ~`Uc06Y5Y3yMOp^s_>hQv!x!bSIbqudk>mA(*O*-rr z3a92P=Jx;fhDb<+2n!1*6d$L%{e=c5j{E_}bv=()eBe1=ortTNXo0(ZBvKT1zSFXD zd7lMz6u!atJh1eFk~cfSu9p@%c4k^u%Ng8P<_r?-Zv0>mamee|$?p9W-oRs+NP9=C zJbhvLzJHl|#RBJ^d#Twmv_&*WkpW*I^mMIPqQ(z;AQ*l&z#fReKyJpNfTr?y4-N>> z185^&vsypHwi+B}Ceqk(uF_1R@MRt_r<+M>!G7we1p zM3tgvZ#t?q=0%*1e;f$d1Inb8(NnqB<6a(u9n{km;MdMQ_L}{4#2j#X7@HWEEI9Mg{>39RWL0h_(Un4jSG^fdnDAt0br^l9XsN=xh}CiKhS zsP_Q$dU(MCCZBFZo#%DlHaoNvit}-CNs9wDr)5#ZS=?91+wd|**Y<-zoVWyb8e2k1 zEb-gr9KoBz#fxy>$ANa(Q#BwS-?Ah3ow_blklrJ5h|5H|T)#^63Eq{{>6Wf5@PDVe z)O<9}LHmvJzZP4xr>8<18WxB+rf9%{)%l&c`TJNWH7nH8Mo?himvWD7)?015t5}2| z5p&Z;-HXhsB_8eiovPLdjH6`N*_|&D*wKpqg0>ey+Gub?RYbO!uQhD zST>lL7$#f>F*FR+Z9k1k;GB$-knK(D-`04~KpxDZDzc1J(J}5WsqX8nwD`qbpC;HI zX^iV$e~rOoVO^Y?beADz7vPa9cq9HoWkDM{ezQC`$rW{Zq*(7RT;r{&?YSN0BVhB^ zq0p>|vkb!`njm|$H#QQlzRclMj{Ex*OV=Pz2UPUWIJM|Z1rCM6df8>Xc7W9v$Rz1Y zYUodbdwFR;nk0mo-roK7%s)+}UnmNPnDPtS2m<`q=7w15R8xfuOHHk=31&Jf@%jrh z7US+X3iJMxxcwZop#$==N+w+E4+vuu<(=ZP`K{xpxY$>fZu)PPZMApgIcU87tyWnc zh;&HYp7>eIG-70>Lu@;F0*vQhR2_b%&rB&yeszhnTV4%U$Ih~7c<6RPTpyLmGt%)VS`Y>j_GTKk7+7L+=-S;1Zp z@4PDYS`_gI41l`InbPA7pf3U)e_%8J$G;v628DK+4uYB0;LR-9QoT=75#xUt=CItg zU(Q?z->JW08B90RnY{U;(&7JLk=J%LuP5?2Mbf<+<^{Wm_UgKz^gR6MGNVvqp*>%@ z;M9KPm&KEFl`$E}=9+dToJZmWlgqU85kLPPcc z{hrs#SrX7yy8CwZ6yDu>(-jeT(+()FuIBFO;(WYjrTv zUPv?&dttDj{v5=z!!NjU!`|rWb|-dgZ zYzwwPuNl~Vrw~ui4P7=X5WC15DN1kT1FZ}`U$vk@V>1zhtAzI=#PGf!s2UCelLrs5 z2Vj(DLY3RY+nO;+QHp;AiEuap1fY}0L0*JhYVq%9(`0>ncj50M31mrDWm$)0=~RUv zHM)b2{Di791f4w<+;&2cgK~;uO8v-#)>u0?mLZeXW4?|)$&|$cbpBL}b9Hl!XPzlE zUw<_Ah5f7j5mt4;G!mM9o8J>n{@~6RC2826 zd-CK7cfvi&;`7?{5neR(k9yj9{nd;>O`?kJ(#36%;eZ!{$$p|)J|@~WRpDn>fuOb;QjXt&5^AEzD8W1u}!Xr&y}R! z+3T3oUr>E{uty>%&6(a#Qh^`F7m+h;Tz&LkU#Me6&{T-<3w=|PndIz;>dUH5Wl2NB zZO~2D?hV@)Q3$P>xIux7AKh-o;F0w4Po;X9Uzm=)PxO9l~YG5>&YoUqB z6KSd9_{K)nHL5jy&8!etr4j~d^vv>gdf7duTp|wfR&$=LiNz8F#knsA!?^GlKYPY& zuQZ_-2CY=p?3GTF)y1@E zJxru26loJB-adQ&Je+hao{IF%8~N-k7R@MgHjbUj9xS}j!ly!s&i;!zpP{;+QK{gC zt`irW_CSb$!c^RE&M$1wgk*_T@5~K{ow?iFev0b6DmBIfJ!_B%kjN}xsxPU~-3>b` z%HRJmiKz~X%x1F;l@~B0x3%i97s$}rjS-8a&Vh$zr%T%2kR%kNIHSQZME(*w9L!&Q zmy=0)-}%|^vvH^;R9r~acRPP8s0Vx7H^bc;zr-gJPA1Y^QTbk0A0QC7`-fv&ZxQG0 z=wjbj*Qi5*2TVZaUexGeiPux#1!L$8uK;`x2m8PtM6F2&foyvf(cB$ZPHyr;?`S@z z#=prXvx~ChvY6dx9orBD;&7O`v9HL&DqFNql`UrEoXLXuU#v$)Dh2G% z2UG-67dKoF;HT%z@+xAYyY)_i1@a09$M^@O_gV{Q1LwhfRr~oUGoVVw@^;@TBqb%C z_`{c@2kyRo17=Pd7ROkDlC<9HeK&IBCAex>wjJs_6>WHX-SYGf)Kh1!tN-lub|?hgBh=9ccK zlhv~cukI~6UJe{=YVVnGup2-EQyY>|K)O|$r3P`qn$BJ%h5&mNaYmv6!GO0L7*fh8 zY4VxT0ni&JwX~Ot3;xS74!$o1|2OZG=pa~F*pED|_Bs{1nNI66U!aK#T*pQtf{}No zC0T9<;cpFHXATUpLEjsn>Sdat~FJo ze^d3{jA8FaetX`HxN<_=4vFqLT5ka>e4a0`?htC|qC&aQjy&6E9Pwn^m=dBX5yL}N zp}f$`CG`(P=)F>+_h+`nx94dvyt%L_#lLV7WSD1#=bVNhUu?8~2?_{^eaKtjpSTo7 z9@zc&Hh&RHh0`-3WG`+9Y}XwKpol9uBSe(L@>9SQJjc(`-KY%^X$Ee(oE#mGm+Yz| zCw6*1BcHD##py1})Oo6K7Jfj*UiljO#A+Mz?n{1W%& zaiQ^wr498ii&c^{joTwxo9Vo9o2z)Q!`J_enP{lE?LEpI;THGcc3^xY!-q7zQhJx0 zmnGllv}MQDkKZq_Zx=-k)l_VvTueof#5L|;Tuh%a$kO=Z?4`6fvNsH~y+v;?2D76aN}x)cvxakRN}w1<0SNqG@n$nLEM-) zN{a5&M{G!wYCz?9EhgfcvJ4_NAkCA}o5N z!_V1&l6^ewi^^=${Zs-6L+~bFvqOywoA%fB*`LzRZ{Baadxu^5JDKh{^Q0*3mkrRr zKxJ4FZF2JWtz0c<4qi%POt!ONE!G^$yweXHqBJygPzPW1bizw>Id$^v^OsK7lp7Yy z)yoB&PlikC)f&_q9(J@x*HN?ignFAK(V8v}KBZ;17!YDCxPTv_d8ePiy{XuVj7Q3T zm7QSIol~EzNh99+<2pzdDmeBt1xLor7MXt~B_D$`e*6=@$qPKJ?!4%7J$0XL774u`6gg}D_RH|?~3y#zf~U8 z<7y@B<5JybR6@dQP!CDh=|Gkt5>9z{8o}1O)!OZM3#e}S2i5It(?WjNhtSrGdm=ackS1i zcC<36AaWt^-QMSvLIC4!wqHaA@j0*cJmCs?kI+rVoQqbqISvisik$X@94QKBjp#e~ z&L<=!NPjzA0&;jMFDF>DgSojkf9lcoG8pj8BT6&Mg;;>E8`|H*SZ>k0d!i768374K zaFrek=t%S7*l2}QP%sU$UA^KWDUcI8iU}0c(4t$T(*j=sR@^7$%t<4(q0Rsz3pHK< zUSB|mT!+JQF(l+i_|sYlZfp!?hwBj0R7TXQuE9blFkA6TNuHJ<@@TpF3KtlqnzL;R zL|pjoz@vTNy8uz*dFm$yG*I9f#3(i{M(xGdg&6%-v!SRx@6YX!WZ7b1uJx|%>fk-6 z(;q+xl&EEB^yBqAY1&A9;`U{=MZFV%25!SjUmX%7EQn9-jEv}-@|<<6bV3OX5g{Tra~j~OQzc@0D0nH~D99(g@_+q&-a zlNkIynwGQi0&K^jBxU3Y?>(CIr@7i50tobNjn>$kBw?u{K|bUL4ds`m@~4}9G^ z8;Tb@A8yRkALo0kigmY11W%5k`lmmavknBUHu+V~I`=k`E%Z!M#)#pehR28NZj`rY zAzAYdI>_wxa4ndZGwk-CnA`jyNOk?Z5KVSc5l4=qs!an&EWJyN+H>}j-eTug@>g^@1ulJV- z)4EqE+TJ>Gf$7_Kt}H=Hp)}~um-e_p!Y9JcyCQiW{Tzqtg2M9MCm7t?)#Gy)8IN>Y zq9oMr$dH@7BoX9ke!oL=>w3qOZ2bB@?dL;W1~R$jKDovpBQ# zc-?(`z4MNh#lC&D(#f_|He@~DDc!XN#9b2_|oA|H8aPLahPiQz0i){v1z)H7sd z>w8~bQO)lMNj(WT5ro93eBU8KKFQS}FS>9442fQALEU9v4}p~_o{nTu-0IB)by>6R zMWt>|lNsa9j&@{T?tIbYp9qgd*tR zA%oqVw4We+3%`9_>T>Y=l;K?xt2d@L5sAGvGmh~FHGVcJ-Q=-3&kWmi?E%qY3}LHx z=dR;-Q+!`cAWLsW|2jIeYR8G93%9h_pChT5X~=D7)a_1QByWU3oP5DW==r_;Zo*GJ zv5A-dKk=x7{$d@aZYJv&iS>kvyh1?*p@`C|7nCIwp%?A14(8WMBppeZFENDTaf4p` z41JOK(?-|b^sxuGzsL2+vJ;wZ%iG#4Y?(@wbzw9ugN4xxp@ts)AhJgLR}uHMiU zd>@tgyD;YYQti-u3a&AMyv^oU+3fgrIyTF6ahG_h7VLaTaSF`JP!w$MRLVoj_hcWW zh4`2#gVNK}XTDdXS#7;`{$oH@?smYZ*tu%mvK(&my?``hqF}*&*#!;uQGpW}fx~BK zm^%oUNVFbFd17Dq7{ycfso~i46P!+=sblo~JV8w&dF_KQf24m%8bM`c>s6s>wYdc@ zv)7TfUDZn3?n$BGkMO!3$FHq5aUvKh5fz^BU6NXBY39k<@|Ub|4MRlv0S;0gZ^WFNW!9RT}_M2HsgnkyDB*Hj(zE!+1+`K;g=SUjVC+F$U%7 z^+Vk@)8*9V^Jf$D58-~Oq!g>dAG=$~L%&6F^NfeaOMf=OOPbsYp>oz=2DYhhjBOp; z@Uz;qWqvMCRdKn2NB$}1o5TyHFQ#5U$E>9)D}L+|<>Tj>70upPa249jG923DbC4ov zR2)%{&n5qx^>yapHnWHtleua1$44EWQC}4zo!3#ygNZ={nFi}`n;3pSSYLIrpC{A3 zRKAjHxLVA(+fyx`wdSP?oowChV|_a@QHovq^FHRgs(g+-T@hXPpA;}<2HKW^e(PcC1oiUxje5MhL;k?6Ia(beo0azMl2GlX`R0llokCqw}#Hqbm#~jccYj4#McxRD@^c5 z?C(b_o*`csE7E7;=@3iUP1lV%r;Zq&CSbt=0@`_-Z@Q5oT1HPD7EjF`$Fl2<<;WDW zASH9sid3U*y~dJx-D2H^MCyA34LA%xuS^7V2`EJ$o5s_i9gj$j!#h4t#Yx zkzx1p3Vm`a(%xlmh#MDDS2LDGF_?K|M{%_I<6;+)7nEY{^TE6|T&<|n?8@(%Ya7-Y zCeeO+YLq!ok;!L&G}10IeA#c9zvKXS_sFoaee~f3AhFp|4*|;GT*d%lLqYG_OdF~w zt{b&9X-HI);E~^rls5Ph^#y->wTq(N!LJ&|qbmYeNtt%MoxNNclZZR+I9-8z*F&52 z0#o=d0{{Kg?M|KCMq{pQEH}aPv_gMf+VCTeNN1$zQ;ILQNvSHww%H{1lW#??Vv*7) zWk_4C=kLXIA51$uW9g;YM!{!XK~hl5znHdc9)0oOJ8FHNQhRfIftffr7B7Kfg<$3v z`+XV%9%*SlLN^aY0q;^|O=6nOSbT!tRqxfg*_%;`fA7V4Dbv1aQ>dt_G^Pb7k_4oZ zR15xCf`do~87`teH}Xl30b5=QQMBo)798V5;WwZlv}m`lq-F~91>Gq{m7u%tt9U8r z!RW-%8SF#$HQF0E%}=Tzogf0dPgaN2-u}8dsTNrUjC;24MdYmrr)H=Vq#x(rB3#F7 zCPa}P7n7pwwvh(BW~PeSo5_1o{BC?ubdKU4*RdWf?Qh&FrThz6wk8F<-+T_6p&PUQ z&FsuqFV?A*&N1lNFKjd&M=3lqt1O$yiXLXl3sg|4BqM{Xr9VCUXS@8ehLxy|Zb|`! zN`<9r1a=Av81j52WIiXzO~PM5wNvi{yJ1;)5&RQs`!!`0^=FLF zKRCYbnfp@l|Izl3W&Iq@lNHlLn+u+RlBttz+eUsMVa5I< zz3eWM`|%f#r}MqCO`o)~vMMHDD68W~ZT{;b zQ%27k&@^k)v1C>>o}3(GAQP>y;x^FWQXwu%y0Q+#SBM27$A@V*zObLDjMS*?mpc#d z!R3w)IYX$Nf$Z=#ruU!HOoEh(a8$QOO*1rGbv*+jdRg9E>wqGtih`)VYtC{3W>a)1 zKhoToBB(;6Rf(vB+7}J6!pzIQyox29GJRV8W&7&$_n*}jJUXt$Um;#= zsMIF!u}xR^$A51sFX{Q%RC-nkAi~YI`fScXqk*!mV@I+ zzOKPPz)?A}M<0~-2g|`T^W8s{hyn4oQl9zUpTx>CYZ2)iENXn<%Raojcr-E6;2vyL z@c!Sic?bAw?sVpHpy2iD3KvCI0)OI=^_l0dOShkI2O>PsX+LA0Nr;Xv{#D!M=W?rs z+Su{si**`SC`HdyDax&db%}!mN~p14>|{jVhuZyqJA)yESCNv*#Ft}hQ6dE0$ztuI zX6y?RneFaUl7nPc-VpkJQ4v~9vym;!PJTA=<`1S??az0GeT{epR(odW=CC`e;LLIp zo{>71ms&+gT_VHw`i~zLEiS-Rc&ee!@N9KzB+?fCLIB!Wcu{(hd5 zR86*z2j!LZ!;xy$3Tt>Qumhd?9lhVIAzPRQEmbI&q!L|Xpm1ZkqEdco?>r{~*{f(* z@%nT{u;=Gt=NMg%Pkf2;bc^w#D`b?yPUf5UF%eM_^n>(Yv{HE?44um=KymBZ?&-~0 zL@7`791V)VYj$7}65m)IJ|v1I$`&mb&>~=JOgcX4m@#&kJwX0i?}a-%Mt0@o&!EeGibzZja6 z(r1kdlfA^DmbaQ_oUkHp$S$)uLzcEpD$4(Nj5UPKsDUcJK(^ch-ae@7JOfSR+(N%a zlpLsWV3DTEkf`dh{rf}HE0qhaSB;##Pq;$s4`_IZPJv#rB*j%an2;)tQRE)Wzr23+ z>iSh{Y{WFE6werhd*?6k{57pe#ye(ThwheAubI27pV9@4YBI@usRSP1tOF}%E|(BJ ze~rOTd15~zC~{uEl54T!p7gW+^CquiI3n2w5=>DBcym^D`?fc zLZiYAnp8#ywY!p%n&|-EJi&OW%-gXhpTB#Heb+AyfIW8+%OOAmc4YJ=oeW)cnzsJq z$plIZq2Yk2uNp5W%Jh=OS*f?jH1{ZUy>Hu`pV%^;%un^#i&pnzoqY)4NdX*gvMy~{ z#^@@tN*4!`lIoXzrkV0J5@x=9-9c8SNf>7SmM0NcBsewr+e0m;TEqz6w4C@Fm@g{yTFvn`NHj zMV2E`$trOTLd7CiOmr;9ANh9dOlj3vmu;JLS^8=5W$^Js6X1T$7A``Ri9Z*uBMai? zOQ?dvE%=%hBu1mf@t(0*68QGHWZz8-;k4KF;izh8Y)Ou-eeLY6m9PCPk!03tE>OlO zTQS#1N`rn7^HqF7L(-^$a7I@A&Hb!Zds%pz6<2CBo#_vSnUw~wxXR&-vA*rLpB3w) zMMfW$o>#+D$_GlOr0J%GGYmyft>idUzLI^8m)jwR;+dM};+aU76Es2nu4i@U)J1#{ zVQ`DYJ#Em?e|Y*w$VZVE1&2!7Ix;ksuP<0i%&4n|9oEvJUPMPLmKr)dlTLs6W#jSB zXfB?3AzC5uGbQznfaevkN|zhGL1ORlFHdbMrM;|DRpn#rUo2)Jcrcs=JZlc@9hD9J zP_a*cB|{OyN&JH8+vP1j$0-`fxK%wfJzd&r2Jiilr@Ca5ft{~qCJ9D(?+n3AM~~A| z2Y)_;5R4PY%+^*QLta(ZLW~&01&~*O-oFY->MmmX^BrpZnYKbQ&5|(Fg((_ zahNN01I&B6CFw?)C`|<)efzh|dlmBcLKqnB zR|v*QXP(&hdx4F5`_E=XTzo&JjpSB^tcdDVkhM^?j7v{JcK0)~2WSRKk*4>~H5#M( zA5C8wR#(#md2yHE5Fmu$?(XjH?(Q1gA!vegad&rj32wnXxVuaCeDCh^ljmXX<(!%6 z>gw()Kg10Yy=edUIr1;G8S#th#u#?F!E1-Df21}G$pR-i}r}w|jp(t1fisL5wfjo5g z1Tmoh1u$nHMvZe{&1&GZFIyr&2aPzJiJRanU=?;}8!Wj#DxTqz$8Y?u=c64>Ah{|w zZfib$$(9lq-d?IP{7JIf;N#h)eF)8A_#0CsBP+ZY0Y0yI1!OPv_G?$3@lGonhL|Z+ zp-MH34jms~89I($`)eRu?z}21+%T#Tw7K#y@&UJj;t>=S)TkOuyxeb@B^Qi{R{>GC zM6mGEZE;D7FwwpFOR_)0Sg!gp7G<;?Y(I1uD)LEaA%qOy`~W|D5U3UOM@fNEDFxI{ z!^y)ZG5nxDUb*5D{Jf~yJTsy=SfxjZu~iALLmT?KD^tQUg!1Szm;f`AxGF( z>ghWC-^1+$BA89IalTE|1M-yR`7V$xxxYGO{v9z8%c>LI`ap@KNt-V;uuJ7(>$o=% zKN+hva`y`SlBUSAL_|J@fyBte%Lw=h0i6We77oHR*1P5Ue_-qQ?;q^mq&e@7i|5U{ zlb69tXp!YQl!H5&c2Niv;qRozy;w?8xMp9;k^arp4Fuv(m?eM-%EF9)jTL^+_9Ps+ z*EiU6eWnxs+GqBDaKGO+7UXA&%l}BKoHRT#!Yje6FC_}kJOmPo+hqmE%yPWXOh4|u zz`ckoC}B0Jlsl!VVxyQ&)FSueF;k^ZH?7$#V@1G92KPF$ubsQ;E-aXi{t0^@VgpM* zpj0cv*&08RXoJaV4~``<$fSRPK%L|6F{pY`M&g9%3BDqV44JK5;EhO7N90Os01}oP z(kPL(KVAB1o}?MHme)Ju11L4>M;|cWPEX&4$6kvSSj$5GI;)I8nZfE+W5RKZ4l54JICMfQ`o#6XwZsQ(h_pXCBRMa9WG}Z1Ht-^7i&2(^kO}PZE;} z0(3h1+*p$55(TdLw=QY#F4wxypkhdn`$3RgY&?HBXW&GHAjhut3>^&>6@ezZ=3Qh| zWQxR55_l`ASMOK7rEv?E&n!~DJ#nAf$p(WOo0*9%7KhO`beK$~o z!EQ&tLPFzNs~ccaGc&j5o$<(GLdLSrD>8Nao_(l=>ZZ@fqmdD+rrM;wvidc&*>tMe)n| zrXe7pjg3|o`%%5qT9G==A+|({NokQSwq^R(AlRUhB7VSiUw*cTfGjWL2?48rGsFwo z4P{gM4ufa3GerZ@3p6t03DL~PEqrPV8vt~UQOBJf*qcq^6iS&8(XB_$;VXMR64hRV z9CeDt%;1(Y2fv2$X5kELC9}sklO_0VwP-QjT!271=grG(r6BUgKL^L36$1^lC4nZ5SB^ka?5rkR zlxiqbR1Y928irnv4EJ@AFC43St5)DDBupY@ft?>7GTayhsiiSyny2D;E+;!l_|^xG+29*UCt3%jjfGarP015Gv`OeAj5@@$3^aF z2r%jCjqHj~U?Ol)%3$?UR*|xi)p74_@b0N6pgdE*x6(YL!eufX1kCIu_qQr*A%F8-`1 z*{|qH=Key25paTB9y9_$0x7a`c6Ab(+E~*~c=QsrG|X;{RG7j>COCVvxZ$C4&B^w@ zmO}}z^(~*4D%W+NyTNoyGg@MRsTMm_7?YiW&Mq0#WS}aw!o0$GIzoKOSzIy&du1#G zk2P6mr51VmQ7e4kt9#Z4kb;nHb{IbbpZJ;<^PPX=EQNOUIh-`WWaUbhS+(zA-*TYT zshnt}Slzc7#i7?<02Os#MvzsuocxTtgYB{hBacN<;gfo$m)h?K+*w)-=*!4Y@R zZ-n=-Zaton+B7ohgd?wl?bht_Qm8fvg>A1CPWysg&NU*@2DfR~fyx>Lf;_TZ;z1Iy z6t}pi$I{!!^ZV{XU2Q6rI9O!$h>dnWd4`-yh&uCnqZg^urfcqmZGh8Vntgp6{ievy6p| zDO0L>7b&e%y1=$7qdEk-CEe;MTN--UtmCh~Jn3|pS{2JxL$&cQ!brjom6qz9v5l|~ zRVF?_AWz%An2aq=Ai#Pvab_qDU2&txnihgD6Mt{3WnZbILt^DhoTBOe>t*0(H$dW4 zk*)ct>oDHWQOq@h%|)uFyYO7*RfEZdp9JTn1NT+&U;@0lc_Myjp!WNI0;{CNqNKh| z@9UCknWo+}xiOcZ!A%Kd+>L3XDV<6EfY)r|gFxfY>G}kcFK00jiKq`~X+i2PhvWvZ zAPgCrrgCe7WF2?Ax4?64sIU(;njq&l42=Kr6t`}ef<7h+)(R~C=-~qHqG-l{n(l(d z8Py4*grxR{%%1LXOf=P;%k0a)pxQ>fb#$X==EH%8*mt|9g z$<&vEs2fprh2tNpF2C^GiXmfhP&K>Amt34M4IR^21=>`N3GwkUkvjbi<^C#rV#zN33uy&%FL%tE;qFs(D;@2Am`2A`9e-DwLBp96T5I+Ak5-);$Y3|>pydnx zkZo7a>_d2uHm${HF-JqR@%rF?Po2rQm1esP#oL&;M3PGoZ(tA_AFPzo&UYj1BM{w| z5J^$tT&dfteH7KfCz!&;(hm78N&jxfOKOEt`jct~llUEGwCAH@Oc)0vlpw^r3_vQ^ z>vuy^)q?FHk_d+p26`l(G8-Ps+dr&Q zl(q1Ixx{MZB)}Nic&)gki3)O_*r$b$S$}2R)AJW84)CYB#9NVV$T%nUlD|*Q`W{%h z;eN(d+Pqx+&E^B#lM3_pUI^rQH08ZfjSyu}&@{zX93>@z3cCKP2u;CrYjrX2tUfxe z-Cuk9oR9}tM5H3IiUXT-PZB|hLQdkt=JI)_`KWN*#7-a(3T5C=-71e@*{Ay%i_9}4 ziqtXB)vR1OK3aI>r9p{gp<#2RQZx}N7t&+|Oc$K{s9YLEz^?o&JmgVorb$GzTDb^F z8aJ>Yg8=3i$2vig8x=dD*}6`FM&4}C|(RH7nEpV&%Ay&JeznkY5-YM*E>^4oPbI3ohj(2=ooqD>eofpYR}FoP^B7*ji?= zL+i;>i_zAW3G}wM>erHWxUZnXEfwO~;QmY0Em8lMs26>u+;twW#WQvDCZjd@b4A&) zXkB|ajhKRZXQK6lh>!eLPpRkHi_6Py$wrM}Mg-igX5CB*BBp9QOG--NlUpJO%#uai z_oBMfHGB~Hkvp#5$`ylp(e{%X`!=}Fb8sNe}vee@k~)DT~xj%&)d$ojSLhD+`?HVv>84NQFrJc{L4{O#lqg$lZ>S>Qsf}6wn8_zL^&&_4Mk>~==y8B$ z7ziUQCRwWVXbgWJ=ufMf({I0Plx*0sVU>ro&rr392tg-Xel3|7YgnjfjYHmJf@dDi z!(FsU&nKnUCf^ek`LWBxIs~5TMBvIlP|LiMbbi);>09@L^e<0f~v>+7Mh2^ zQ@vBI==l4t#I;M?XR|3Xj2(~s!l5~Xb;!CuY;-o1d4)sP<0zbj0fOki3%L;!X&a!J zAmJhQ3fH}J(i~zUxv3|L_jMQ!e!z3hupH%opBl7Kvbi{f3N-IZPNJuo; zH))Y27Tg@8t`1D0beijihJrxjO*Hp1lxL;wjeP7l&G4w;Gb;kT-Xm9 z8K%#$YX|o6S7f!2Ef?_uGzcs%wSzP&I-x9bk3|#OA(md$MGuFsh{wbxfpk0*=oIi! z=-a}-q0*|o`Wj!VyNO7xDX?Fg6uFyLzpJ;XCYYJ(sjz%zO1 zLP41L6W^%~5wO9DYRaMl%_wVj=}5s^lz+Ykmgu4066NveufaXi3dthdh8*7Mx~BZu zD^oa2hdLj*2vhH1*@=rNm;4kvhv-C?BW<3MRh(bid5BvdrcmPg{o(2(ej=RyzdBGqDuchB6V+Me;GortgH}B=?M=+s(03k#v=05Wd3ah;2{)rLl4^ zTxj@9LYOmeglm!w(5Zy3GOpnkv$5P^d7+CX8T+Ex6p^9+DVSUwBSH{v^MMWU`7&L9 zVTci=4Z9f=#{}kg{T2g1eLKdO0p*d+rzT))E1sm+9NCbT&#W=?-p1zd>k!#dTNjIOzuMvN+12j;NpwcU01Wne zbg=W(#+ff)59~r>c|{uH!Aj(l$|Q(Akx^9U(~=Z6f!j&0`SvhHAd%^RaNyM8q8O+! zvdhey{|?e9D0IM0|h~w&p|B6uve;94)h|z&{)eH z5oE*O$xd$y2C#Yl|X75zcBz}-60Tf40iQgsj4_WeOy zKbLWtmBzn*(J1*Msp3ro51GABdmdF>CL)+|pwilK)r~|=Mj~Z-*EieuA@Zf@&jVq_ z?(g(<-6Q-ne0b4d<_A8<-QVF!*&60e_DvLMasUJ5DV!3IXYiN$Tfn4K zmKirg7`zrZM}RMNWyQL~llPzBmRE!^{$k&hFkt?-z%ITbE>hOZysS+6%LLDe8fOF{ zUWKZt$iu^hlhQMROrKCcQ+>)NwwR}5PC*z>0vdjB{q$>Lz^6x~FUmK4T}M^qssj(N zkGjQ8n*X41%%Q@y{d3dFOq?X|1XUDnOZc!hmhbY}VkyTe9wVkiJL9UHzVO#O$jRn> z3LuvPefwxL0(~RinN<7qPl;*!Y`d_ADNVYdqyUZ_=NB0a<+Te~j}*i9zP-O5GQAkW zyYT(fnVE-5eCQA;R~xOIi2R!kbCX8NtxWE0b4;7bcf5dZ7}z8aJ0(Sp}9#QHv~Zo*J~z9;y2&E}zo2h!hz{1w;~DBN7Po z^!;Uw%%x6>4kv2Co!!7;QzWQ&d?3TZsullprembINz7W39%;hU^OG`}`Jq*WF_*NC zzdInd)1u4UYM+tyr(4vh1O%5jC`*Q}%m6Vp=JCLm>x^7Nm9U14j2S7WAVZlu(n)si z1SdyQ41F&G4k6kX6G}#jLRiiOq;o$cuFRxHlnHvz{rN(;j zC6%8~WiWJiQ-gth68t^(R!q5Cc-4QIw}ocW@O-miyT1MOudKX&g}CWGN$?U?xRHn~ z5ZGed60OiAOoc&fasz*Gz-k|IOvso#sw@(EI=Kwf)puvxL7W1;@R>}^!(-_(;l}Hk z8`v>N}#(m^h=9+ZNMU=`g8pbj zho*FFEzBmv(8cWr4`TwmQIAl6+Y;G`G!NdWcQ!u}A9-Miv@R$0i*&qGR+Sjd_3b?- zVrcU%2F?#cXgum#f4eC06c0#9uCh!l#IQh&ALb@e(GtCfpSJDYLvZ0?HccGUyXVan ze*KD*p$yZrw)Xf&${Qp_vyD5`Uo10JkKTjc>yAFcOjrVZe>EBGv*JC>s4dj@-@~OtLr|97--uy_WFs!@G3?Ukhuy5d4e8Q^0&S-6=NU69 zJ;a1ZeLnBQlPI4#6*E}cL|@U<<{G^w7k`(%@Jix^KVL-Thz5sjf&~FDkIPfXZXc7m z!RlFMUvO2DX`%y127Y)R6(Up`n1j)pFdU^EQFIoWw4(=s6YW!<8c`nIxYI-Yfnlnl-MhN~rHl^KdvfvYqD<%=@EEs|l zBe6TSu=RMh$bMxW@Lm4W8AZp+S6qA_O|S|9{wjmbnJ?V<_?Xk?*UwxWn zKw4aeG=9R%-$Gx1qr}h`n+TiR;!aBIJ8i+NT_XA;iZ|rfkG$R-fOeG>; z<(Kr4sDc7*DB}OwRngP)=q+odxQp>eyTN*TJPfz~BN!*)ddh#wscCkiWVQYDET3%5 z8c7Bs8{d3yjwXSR)UwG!8gV|Y-q>%B*l=eEH(un)3nfg7-sfHjC}1{sO^uMls(f|Teyy33 zoP7LzPc#DgVuN>BM;r=NtuHGjBZC89TBcgU#(Nu%kzRakyM*De-pXw~w=0qt$hfzB zy=yt0NlQX8|tVNURO%%3$_T3-Pcii43^r9Moa`T;foax z&H2RCEI~fns%dQ%qr>SNmx(VFBW<45_q4I*>+QJ={8Ffbuxa*V$b?xs zp_9Dl7%6^{LE1E1Ue{zgyGIBeT5Wb88#|RjKQanYvy51eS*e(*8q;u38HzMCe3O+U`wCv z?;A7WxjGyY^WzBmuOA&N$~SdmR%XC48L<$czx4& zD3js<+9!Zi|Ndc)|ItznW4Lk8+s$~t_$645B2TH+nD0kl*;!PB;4i^h}m zbjxh8^P5AjOg$=RY45N zxZOgsvX+yUM?$_+RFI~nW%TrLsb^TZ2{J!z)B&s6RFQA@cy~b<36WvxJi_>2-QPc7 zWYj!~82QWFSBcS)RvGjP-)m_aGUielI0FJ!%F5t)Fp6vemCbd3%VlGPXvusX-7l#@ z4sSI7(yFRtnW9O8O(djftcQmXIF#DN@#M@?mb=&piSOOGS9-vj6Xk^6q_M?|Qwj`N zh}OkmdJeqyzBEHkO-3xl?bi_whLx}lLC=rBZAiH4G}tk7Xol=2cpS#-%L+tm14&Pb zC7S-X@7KH6KNSf_*7(?)nq0t@Wwv#*W-R#fIBiozb2d%T3h?40r+lEm7w;-5I*iDR z&LJdJM7=H-?cE`pUnBSTI$$}Qc^9qVJ|pEqkvw#cJ}b64Hozn$;~}ZnIhYI<X!HRupiCCWu<;vpxJ>e z;PdZFiGn(yDrH{z`r|t~aW@Fib*)*ly}#FZbk6A9kC*y@6Jc$fHGBJvT{G)wn} zo|g&}KrRwAGdYR!O*pxIPS2#+Ry$Aa=)%g$0v@0qYbm|Gz2echHM%Xv?=L7d-7hz*h5@gB1CFi-op&Fv(X_M` z*%|DwzvEkXDgR0ea@dS7R>7eXW^%Zn0P5Gj;=$ae!`7`uPqMdasd{@nKnVF};FP8<#y!Rna_tw!h2CPr({~VE-)77FeZINZ&j~COVHOgnC?jf4SpM zTsiyu_w6oGH3ae<0+i$1tw)Munz_a#uB@!QGhZy1B15KBBZ3bGCwKV{m0OkwVsDUR z4AcU^0Od1>=F%T@=-DdU)G|dm3`7{NxSXEv)E=)4O**3JVrpiCs)_>2y1K6`xU_$4 zn#4%`pPRfAiNA2#4A!CveoPDu_zklvI+Ip?92zbb6&68(*7fv@<+BL;>AAbT>7C~f z?7O`0xja8a+})exC~{r~cl8SjD&bBBigGN?_m!~jU)G;LecGAN$!ve>LX(KW<8ZqY z3U9kzr-UbEr+;s`&!C~94HM}L^!FQy!xsnIuXhn5r9X2zR#aB1a8td%`NBX#j4zA1 zK#~b3C2RV2+YQ~Ha)yZ*=WKQn=P*^**WV;gr;gm6d_09Z(^)1zZSPK#VT^Wi8g|?w z&d#an70kZ9tc4#j#1k}0)Xd-3Cz@*m)^kiTD{_fsvo(&F%S0V>HCuuC4kJ?=mQz`r zc*rbec28GdMd8q(L7DtM#;;Qwz%*xxLeBAW-oWRYuB6O_m)GH{WOa3}IQNV7V{l2y zC^zK^a6K9>zIp6gzzT00=Vj$O6N8*~hbJ!s^lm5ataps_=#Uf!bJm+I4fXZAIx=jP zl#EPNZt`Jni3pQq1>aRj0(?!7!)BdQ$1YAzNDfDXz2?-I%_{j1dzEHs1 z0*3ejFGH*eZjZCHWZIrK=iTshP}i0t1HDlpc|0y{!e1{ z6oYCpzx2Gt$Fb|FMtDZYYYhH0EHpF-6pr$_{aZm_`1=5o&Q{ zU&jl?lykUCMh_*La+(ST3kwO)NNM=l1fcDyx8Hlx8U{{Z;+6L z#6kT&Z5JPZPQV3tz4L8SxA_z$JoR9L$MxV1PFC=AiPGY2c~+Xb%;%9viRuhEk?hac z*CPspmS$%1c!E5Yu9V@%GP2{LA`p$-0q>KK)YQPWl3l8_e7W5A2<)wZrw>v=!!KD1 zp0unLo%treb0Jj9+%7*tY?tiek?%BVuwoJ6eC!5mp(5hU%q)O4hqtDejF_-elWf=s zGS;Yhc@J+HHNSg#O=Yuxqo?_&xua9aq9of-!&G@WB zg;7{oEFmt=2lNcsYGQKOZ`wFgeqr`46{Cye_4fVq!x#t~-&kq6+3n{Bol{itU~Sdf z0L-!6x?opaBFOH3-y&GgL5>urrw2R5Q#EVl&SRzbzEi>;5jV)sY@Pp8u|qUs*Z;*l zE4sn;yVl`q`)cRg+gN#dZLI=xS9^(J&-Fvczvw7rKq2=}Wo0H2@1qhK_u(^`O}@t9 z9T?XhdvY$wA|yg1iUe-7;OL)s-lqP-!hT&TK108*CQrTqgurktepY<4`}X#y6mvj# zO>!ajho7}Zcal_ z|EY3knp;`1&syo+>D!0&e%W2_DJsMaXK>i(nX_vku#c z2EtzC=5aOzCJ8s3zu#FZ7w4R4o|gMLU5_WN7Hfk&){E2FN4R#g7(RG~ka!2EX|RlaPM<)(YxTM?{@M9e#OvNt-Cn z|7&7Z9;`kfX=7|0ugBo&>>M<%ot2phPyprSkf5`*fSJur1=8fsn=xD9!sl=I%;3J& zjub=8immjkH}JlO0SO5U=S`}5FIIx*bG&pYni_wRjjpfrCC*~u;MjEFbbShkfJSB5 zV9S3D$B^XmDeRs#zmDp-mR0Ikg(dKh&)gq1Ob7x>5n;bEx_F6=KWqaZ#fc2e7wDF z)aCV4iHfY%@MgmY4W7zoC%C0h#k!?>R;TLX`&*f1b~;V6bau}3#2|tx1gOi~gYh>b zP=-ura&TKZDxH{^h>5A(5rsp7hHO}&!$RDt-D-6{{aZ9%)AOS8ue%Ev^$!U->}s%@ z?uM%AxgixsCli(n@LmO?x6$XtN?k)`ZFBO#m8+|n&n*s{W4{TR?U>>)$;pd+M;FhQ zn>16r#ti!Nu@Z=`%|%+!EJj{$#&u0C&g0~6(!NZyx| zamG?Y!aMISF`6*jK<(ZHGH`NM$ZqSXnOX(AWE^o#b@}lUCoE|D;Gq2~tHt~N3<3mP zP67K1Eh_VK0|Pv}&_TveuB_P?lU?u93er9vFRGZsKfgM4de&saz=Ux+ZbE|A$9~Jn zNrP_&2N8CMK_DK_ldEA}q+A%h5$2#=;~#Ovym>AvlL4phfG={7p<@CGa1KLc->kxq z*UNU-mkj%v`1ttF0xko9Kp5Pz)tERAv_B-Lr6pzi4?Gy`-@RRQy?UOnj4;XB>t{6- z?7FfRn`Q>Q#_9W*df)pRKe&Pd0Z`*=;LU$=bp30aq5I)O}h zyTNkuXptT;)R{Sb{KBWEuPQ%(pjtA0A9;L#g9Lc#ct4zdJOmH@yz3Z<7!>t?5=zC3 zf~ta$;6umzB}FzB$|~DHH*ZDK|Jl@_9>f&O82c=g#uR2R*QYZ%S`*NhL3UGed)S=x zgA-?56UFt0amCu{AAO60UD3WC@zHo_VyhCB_m2)-l2u6Y)$n%lMg-(-Qe;nPx{&OH zpQLy?6w~ry>X|S7OqHFTZ{v)qyIj|Ud(uvA-vI4F>Aoc!ks!bYHl)vpjbPt}`^&87 zKzQsc8sXVOh34T@7BZ|Tpf0IJM**LpTNo7?Dza)6EL0 z5GJ(&k&e|xqhcNcbaQ*_l&J?K6`KZtWwOQT;<7G2n6_I7px)#tSmR9T#u1?*qw*&# z2nGTTYFaL^#_Lte)K-`K8!V@*v>TfJq?9hx(iXq4@f7TPS5fbLgx*jSJWn~iH-rVL5sn}SKP%~qSD;sC= z=J5LnS5-IR^e)=gAR z&;*mD4Ijy)hZ_^fR9A&)ZmlAYN^HSJ{?rmd4~v-VQ8wWYicg3sc8W8;`8$} z5QKZvrmL%`P_82{!;SGMC9%Ve0k(k(Jro-5d@Vq!V!`8*pzB}>MYD!nEHr|GjI0R_ z)y36)Uo>lnt9t1aFd@;+*<=Qo$}k<&5$C6<2wiE~)g~Xb$As)5rd%~DgS1;cw^XVW z^Dj9}9N$<^m)t+ol$XcALN%Uq>Ou7au&Eb&Dld!o<@K4E>pK@O?}!moN>WNcRX9wT zxJ4q^)ukB_XC_mrTZpoP-CS#Ny?yufO_B+lwJM~NDQb6Zo3v=qV;Eps2d1$JlMsD; zBGv3DQ+2z&q4U(J?qEYEsf_U~SO{Jp&o{_v=Bygo4wkPIxBflduH7D_Bx|avtGBgs zIj(oA*DN8yh$N1tr5akfcBQ`~4=4e)AAMMijEYkgKdY;&%PLEebFdIWfsa*LS$euP zw$~j&R-e&a7h&}w)58fIcm1`kZr*?HD|WG2G_*XQ#5_JexZ$u9&{I6{+ysB=uzx*S*lKI*NJ&YW z%H}_fI=2BbK*s8>p|oKUb67UNIocvDOJ_a6mE+Uw)XNDSYn4<9w4dH{eUWlv>S#0Jpu2 zQQ((xCXC3zl~L&#JR0BIl;x^AUS2nN>o0_ZO?(UO|0D`N#o0($EK=sse}m^^A?;sS zXzI*|M9u0m>UD}CF?ynk{?VkpjQoX(B2igLS?O({B*7h(D$VN}vInz@%hh<@b8Ksx zk%on^xRr}Hp!KdH=?@zm99))yA;2kA0J8wvdYSs`8~SSktbzLBjySEgGp(~`dj_HL zDH%B(40juhlIV{kq8z%tjDS=iG*do(V?AZYtkv6i#%<~RoJPcv4#@@D9gJjSs1}*T zv>z1|UO4#mpi@4hMS%6atYs@3ttd>2EVz$odvL>!!#Q0Hz?|0U#Z)xZ+~$k7u6N$= zjDr90@bQhgX|QC-Fhcr3OPo60<`k^b2_}(Z45<;}VXLlrd27#&yCzsjR^jaFa!Lbf zZP=X{k3Whk=kJG>}+D=oxHfXIN;1WI5<@K_T|0W zIy;Y^tuX5KINvrXro<7y<0ZXJ9z+art?pdPH}GaSVErN~E>I$?^(%popk-(8YAtNE z{8{bNM%WGSq{f}i8}Jo;>l(fh^eKC%SgEP030NKlF((?{oCORYTv}az5VO;)Yi6ny zLWL12HbhR&6*oYuVqg#-pN##9*yBvAQ;*@_$+%*e9=$eq%lOz>WlQx=^4Fgk6{khA znzR+=K;X!WVWehoc6Ww-T*Ys-H$omrARA?>Y-w2n_yc>?@x8eW&gdA5#9z1l_U)Q_ zyH}9wEPPy@D#^*}SmJ&B%k2`9?@zZi85|=~V)+iU%8Rp{_~F#lwD-67((Z*QU-@*bjj2?8{G6NhO4 zKvQ6Cv9D>^i#J6eZ*O9P0TQxyMAK@nuFl>XAG6TfUZzQoB8Y`Gz(O9NQjinqwSp?m z*Iv9~6psgj6tr;Z0s)XKf_ify-X&rQH}^1_j$by<`H=9!Q%225>!hkQbWx4zyC8($ zj6bk&tv(tiBP{-PTJrpjhuOgRM>x|Y#OF36ANGB4r-ezs^{C5mu`-Lr{Kj*u>R>2x zOqsl;VSIWz;&vk{N~U0z3L`p~h>@1o_Q16PIPyLE?~}jdzkOfJ9aC0US1+2i+V-X6 zd~8-CsclG%2!(dsyz6j3Cnmn&bXYH?t3keMp-L3lVCOubUa-;AS`$T{4 z>doWwxdR+&n3#q`hP%_X3{vSX1%`)Nva1Cl(elJ3bJPnJSSZOdVMj40#!?;o`Qsle$N z>j$7qGHK#yL4Z-bKf9xRHg3#j)%|C^;du(Nm&7-GXsIQ89v^QEWQyFLA12ep8(JxV z?SWHMyTvMIB9Hr#)0x)WY{3Y?plWDpdY)-2f*XLHr@i;3#cD0Sg}D058UV^*x{x8xSApAcra*n0+^3#Z0zLJ8ZvY; z1o^J3upbMhe>nnN;(R?rjL9rPhosivaoVvKhqbTCQk`90Scu|MPBRx>a6IlONApu1 z1{E9i?j4G}mk{r-I$N5HS17fky-hi6>2d}J*&p2k&XPYp&sT>wCfT{k=_x6f#^(!) zF5&ZIbHi-mp&^5WNYDe=XQVn@_BprwAN2wB1i0YT^%(`)1^{{727@;*6b{9R||J{lsIFq zpq9o_`+>+zWK#lPr3|~Dy~jmonQJA}nLc)ZoU9Nc4rGFa@IDbYn;u>S_lrd3!zZkJ zZqEQ%0uN`s)64cxsqwjJa2{y!^gbt`t$%d1uC!FDWWMKNOqMkM_y5Fo2=Ja)%s2o% zkiqpYiTP*j@87@Q_8QEEg@cNU%z3gd_Hiwv#q-k)HfjuHzyBL3LM##~s4lX0wa`dW zD8Y^v2bTA^*nGYCOP&azPq(Yy6N08L5g-w~dd%S2zXqPz=)_pObkUOI`Wex@Equ%V z&r!4T^6GhA1%tZz?Gb!}~4K)&AO z`0s0%u%VU~o5PtYC{Uow$DrH$J|bsvIEN2tiF5Y`USC(AQz{zj022^7=#aX#9-2Zz z>T7GIgb;>*63L@;4KTTY=~!6oG!6XC#JIwDII7PeQ9q{J+r85*$S@Gj6v&r|(zd*9 zO~ShTa?-Lp9*EQaO_`*OkJidfo$EQs(P;)f-UBJ~Wpah4;M0?#gFElsutYQ!nyQxo31`3U0pcIruO#LL2e&3e738Tro!pe0HUKyLseB% zmW;624xzFIf>u>Fv4XOu&9B;3W%liI17<7>3$~`7XFM)e)6%a!#9ADp}RcF+Zd^Nco_(1w>F|8g($%i{(1LU%Ny+dgo&8xQv;Cm-1A3RhG83025Q0-DLs^2^lv` z6kdN=lH>37@qCz>xj{&n#HCP+Y+F2uDzjfK-}CD1Ty?oSH^8tyH7P44cQV_Rfz(2X zTb;5OY@wOS?J3YMYX-njpqy^#ZLDG7FDxI+)4NrpSwl=lIklDfuUd}gGpo0liAt0b zSyU8!2=6rza>y=PLqbT~W!22h7misE+r`AcyGU>p%mT%zw)HpPHoZl5p7N{~&ydd^ zI^HTOQ7d*AbL0u;%V(LH!cecWWEc%AzBhlsKzoRTe!;hX(Vsb7)|Ql)7}y9MzY@~3 zGZXTc@Za8iy+`gscLJiLqH6Tyj}|IA#`+*YN?K}e$2SYzU8cZ)>YGcAJTN!sxOZ@X zO0aB5f(}@b=ll>P!ootw!u#d=>Y?Id(<9J@jp(zy&1yM)xW6AQmXAs#BEst#tbXeL zqG~z4e~8=K)(+_B3$U}(x639qG++jprcEBSno_QM`2a=ky(BfT+-jGurX!;wUU(GA z4*=_MI7aG!JgX!Zj2$4Z9<>8YCz7pn>9U0zi;E8(G8$ZLR7AMzzbzD6zubXcTO1Ev zoSQrL4)z;Bcag{7)YJfaW^1E=-7_CC#>-Ql`9S`ZxLvW zQO89^)mxIqnUmL<|M%@#7CSi^=xU>(>0cYi1pUg))KSq`tWiW3SB~C%{YCNvZBw_D z6A$=|$A_Df^)gZ5rm(Sj=?!|%C1IqZlaZ2&IbIMQ6;~afX5{jF=u{|)zQAa+zU2rs z&hfnyScPk)W|L1%Noj9wU7z2A1_U?$Us(Iu3OXp-U82?O^U7GUJD4a8l~8@Xk#%i% zKOGLkjpF})1!x5WCGT{0+v3zzT#(Uj@#M%)B3xNAiI7hWNNjk>0zj+6{i0r^n2MU( z-7TZ--29w4>1?hrREa{?99ug^CJ!zyZouO^8o?b8B``ukrRx>c%9&|}NzjhD`I-3AxJ11clzUX3Wu-wDL z+O3|x-p8NFSAcbIcGzk(A6MClic0&JxKmlm!|w!*;f2T^7EpKbwFcg zPd(y?TS(v4;#(L1AulI#UYAa*HEir;O&!e547yUd%x}b=4&Pv+%^^~4~wSZoqwCjm^DG(>j|9`Ci^gb#e zwJH{PczORx?{B$|tvzsGLq$dPAX)v|DQRv@@^9_PSiRH8uyY@YVclY(`5Kk6&@S&iltd z9H9)a)0PN%y?u+*c2(+uUV8?*DiiG(-nwILPc*J!d*{$whWAC>{uiEp@n(d`QA37* zIuyz6ZEe`txVCKRng-eP6_TF*au@5cdZv{Zh_FV1t*@3*j4LlHI*+Ip+mAnNm}=+Q zV1wbHV}e1w@c*s)2gDhK%&>@1j4xeaBiQeySB>%KkM;?YmB*Me*fF zYf+>TE%%zi4|m4Of+w~$kqj%vM@iYfP@@?Vk4gU zF0~i08+AX@FMVh)9WLVX#nx{ti)VkXs+h{6I2@ZeuhcHyvdQnYl)AgRXR-eu+TJp% z%C2i0rbALhK&2EEM7l#trKC~m?(R@J6a)pNr3D0}OS+{*y1To(H}Flb>%O1o9pAtA z$2*1t7(+ID?{lAP%{h-+3$e%d8?A0?4a-_(3AiwnQs!6F_>{hW)H$)AQvFg_jqoPr z`4}BlLcMKP#rMIUa;=l?K2aZ^LYiR%{eiFk)T&G|lSEy{4fm$~bZJFDQ}Gy|_BP7a zJ2FjS$jii$5s^oJjfCTW9?6oAWV+x^{vpen!JPACY?#zeJI`32iI!=!;zUWq3H*CK znrVDh5mb{HYdvL9V7+LrmHLiId6TcR5EB#ED*G$y-ge}0vOIy-f66aB=0x_%#5%X2 z;a2X&?9<8qD@75SYeBIV;ajFv3nMnyTO+HX+X!3=k<$=Pt^Uvof-6zx-c|bt|5R@g zYIPKbSYrY;H^#Ea#z#wYH&W`wmAOKYH2H9@evtcS@=OdWZwB=?A&}KCc?@oquiDsj zQVD*j*S>y#e9d!8P?z!SpHI<81aac6=M%Ri^s_PVbM9i~PfVNJgNx{kOCLOP~ESy?%SRpjF}#`XLN-+x;2j}pi~E}>?Zw{D{) zDBZ$BBWu7FrwK8DN7xyNi@gtMTy12!!(r9WV>70#typB<5|bw&H^V{UIbH1Jrl{DS zT~JRg6>fz5bhI`4rw1B)NihLs&=Yz_Vl1>o{-0DrmLC%nCm(5)2D?R=KfFy4CuFCNm1afGM8RI<^AFWJ;;vk(R5 z=J-|OV)%(r7qT7ii|tiUalgsq4Pj$6;Ea3mEX65!7dBjK;u}`ZxaP+kPjT7`gZ}(U z9=Tdx+B)h+@Vv&yNAiZr_#u^Nl}FpQ8b`9(C_N5RTHnW8Xk&%gU-C+!j0V2w=j3J* zQv@9>lAB2r>4tUCi)Wv{)%8dRh#(e3P{{tyXMeW{7SwHSONuE{q(YCCqy;*;e$K7A zZEmY|LoZ>SuX@*wfbEeF+&i?7b>IA|2rSzQT9R1t_GszJXwfYsRAl5Ud3m>I=}Jb1 z#(7%hDZna?5E1R)2wG%c`>6LEkQSWFoLdp$JT_C>T3R$jH{1?a`K>z*Xr0=2JMP() zw`}vjJOA8nsr)$VR%>uzP>?rMdk@R0RnszzQYi|A10@pnFV`!I(wz9G{DU>7-7k&2 zyo8ZNfZrX-sk_JK{u`L#cN0e~pJFVshYIRAC6;E94A>>!2RH@ZU}0e9j_;zC8vZe4 z;ictupp4;v%Ts78Dem}vPR^%bjAgtU1lc^V=Qg5chTAfgo&j+CuCtx)*h@`l?d9aQ z{be<7;NUFTSAUsB3Qz4_*J<{`SnTxv_saR4lA;<-1DniaJ%0E0Z8o#{iuv&Vo_bA1 zHMI|Vqm9ocJk?6NduD7vQ+(sYWl&TOjBoFAJ|67v%O~**>Wa+Fe*0$7wgB`rrh(AX z;^dXn<&J&jRIlhISKPSM1`i3z?FHVXXitx^{V2u`x!9*5Iy{aqx70brV z@!}}jN&xgNB)~4-ig-kbR&-cm1QZ6G67woeyG;L$xw}kq}8_X_i(0RDmuk$YSM>MmakM9y#I7 z;WNaC4%ftO;mW34^L=P)-Z%fV4PEQok{9IXj}2fP01 z7cIBrlhwYh(wG$_;PzIJ*EbIEDf#;!GW?PmxZ!qzZd1RuKChp#_A51YbD|s>=~)U= zQ(+%X0EpGGk!CkH5kn(6B_$3|C#Re@CuySEr!{Is9*~@DOlUBKZBD!Cid+jdii-B4 z$EE(AoJO|nzm&LMe!`{wOJTI;pj8wRQq-|4=tkb9ABNtBfE50jt;AQ0Tm8B!$M!_mOh z)bMvUb;F>AlDqHbadgB(e|URd7}Yff_K$xG&1pFbQv#3|WEke==4CD0qs3g@SBsBv z+j1(em0pbyXwCx1bI-C~E9IV?#&$7JNV`#Blc*@URnL3Ke8p!7qT)8~7T;>J z-OyF%HZ(TAIQ_ZNTPJvkOK<~s_vE0Xz1^wCQ!+|znSuFmj)|UJLioBr+wvT4gOD!Um*hL&Y}3m zt&WZksqpZZwC)Pb0+r-Q-v-tXF;Jz5f_*Aj)B2hV`tS%g7>PcGzv?F^C#Eg0i*Zr( z$BXDNG7oMwsEk{C%dNcIMud!wJ$$LgI~c zw^gDxOTYYaZpymIypc6Dd0ID zsh*cYpZ%?H7ld2M{2pa}vr?QI)NqObdo%0j?~)~BGK4SM@G0iylj`0*@k^yol&iPs z@l02`Nw`XYL@gOkEg5ZedFj4rf0pEddE{CvB4T8;k|6B$*Z6{qdjEc7X{nRj3Dx`V zAy7}|jhLTtAFlW1!3^qbtzD2bbyoZ7EI1B!j`g*ap~*B1kSwcP3=e`Ca^|7T@t@k>UcK2E$R>ywocX=!QN1vp!S z?dlc3@Lx91ldg2C?;WkXkkpZqtriTdK`Z&UJMf$Y@q+Wl_)?K#6O`8QFJC0T7`}Wt z4(-JG;WoNp!kN%rT>t*!VwRW!;u~nV9dPrrk|oEp2{QhCWx67FflscExRMx1j=MXa zWl&$o7GKu*%Bu;-Dxpy`GJ;k2kI2ozMryTH|zjaww+u^iL$AdpRt=bOsZ z5?~IB(unzu#oM#lS3AKWFZ-dxTPl42vas}+y?P`j=I@Y_&FV{yin#RkLt(?USlPNwG8}897Oj_7 z{X)*mJloF?Rg-9vi~rBm)SJu^OpNok=0h1t$@RMwOA7-b8D>SQ?pFvgI=alcCX6Nu z*R$?l`22i)&O0Pb5z@#_{CxZ&pl4dY6&eA|Km`;eK=j4aL>Z$$le0O3pmvzV{uG%G zi-%{#d0IQ4xf`}yN+21n^Y1r`9v?efZ(E}T5r1l-5GYfj-=v}W1-#y!IPk&69-h9n zVd?qug*yG2kMZV5FGe`BU}U7ju-Fa5vnlA)r!XY(e~Z>zk@fZUpXzxfB?o^mEwShA z9NCxBYXn6>ZpMDT$~i+@Q(n5GU*Oq}k-SH@s=**~_DNnC$Pgr?@9A1P`+amQ-LXRj zHa7Q=5drJZs3~PlGpmK{eg5-2B8U!ee*HTC>WVH_ z<{V_;pzitof-z)X^gPE^KCTjRkkM{T5 zTiX8}75+{Pp=kfdAA?^HL;5dz^Us5x5>fy6MgINmj}jw-fAKB)wlomDSS>bMTOqptQm*Y&8EVDzHDIyt|r#uqhGDF zZ~Y1bB(8m3|NDZxxNY$KeS`{U0)z7B7wnxt^Ce zu&PSmXJ#H*J@MQF**-6bLlTib!i0(;Uv*h`eNre}3XECExII&n%WF$yG!)e#n*0Pkc zRR1bfP<&>`OD*6aK-r7qEjW^y=~QD^LBm=!txvPomFvL+7}^?m_&{`+LuOE^I*e8n#?~cCMGjI-Dzw5&4aL~Z|DB~@F->9PFY0!ax2<9aI(nc zj(^VCM96enS2}-V#I>8-wQ8@mKciLTUe1)?r+`*IRk4@mccsF6naxe;PRv%eW@mdX zms97%e;yttO;t;!_m`=Jfl@MAXH*~qQ!3o+^`Gfv8Q(XC#zr$$QKAE%KYem|@q(-+ zHbymC5ZG(YM>seeoiq11Wp!%YTST*Pp%V^lX?Y0pag5r=mX<8(*R-6njo}>ZC7=au zV9oWgElf@(NY>WT@mewm!$@#qB4}4+QWSPuRfE+XxY!gSbj{8#BnT^${!f(!V+W*R zxw*RX^6{ykJYakLkVRMrv}gX#&JIpKcxh(*Y>(5+%7*Qk{n!((G(ccvx5a%v8WJp&}wm$yENX{A1xqfk{=PKr#( z9$0%SI0d_Me6;eLHy&K~i=c@iaN)72)zZ*#w@oi?+(5+JSZpccU7r~skj%mM zI5s2p$M0QXSC5<5oZGLEN*JFIV!=F7cYSqHQBjed%|PB3^>V3DuL**qXkt2$k#66< z9Y8=eY+mZ+fB2#PyTx$syHJDe$x8TqBO`0h*K=Z6bYS;ZW{3g=Axx4sO(N8KtycG5 z-+e4$@x!PEP@FHKw(Bf>}z(-g!$>p=PU)Sa%c zP${EhVu~8z3NMq<^@gS$E+!5q`Hp>IWA zHhk8rtf{SScfMJf-=_ur^_iY!Ze)usZn;xkX`~r@EO-H&cWo0C=p!X*K?!qpo9 z6PrLmA>g!09rY4Uk5lou@ar>;Z2!Kr6vf6SsGV?0E(K>5o^_OX8^vKA<# z?i;6TZcT4Km$#7kmf@v-FQVz!FMk3mN=iz6FQNH`g#zu`yO5Q(mYF<0F|o3|%njef8Z19bBBEdQKTEdbf;Vt1y=6(q z`|bYasIK44-nv#6UrrUf7Vx@y_U>J5TH3;Fqo2^_p@9jV32u{;k`hTx_3C6Lj=EKC zLBY=ESc$gR)*IfeQ>)AKH#y3%PwKJ9`uh38kw`5oN%7bwq_9e-vTf?R+AEoeu-(84 zvz95{^y`_`qm9BZaJ=hP{7`fPQDBAJiM6!w75&qvpOYW|HRNuM>)g5(@#_}|TgF3t zjLE;V;eu;>s(hy*B(a)4=o}o6N?B;!G~lk4pkk4gl?Fw9U7gTChOFApT`g^!pPY|y zGv6TLs3Pr>*oV5i45b3Z|JOpJuNFD0bS42=EbNR}BoNN$b7l1Wb=r4!h7N=4#xRrX z@aHvSj>|3=;ry8jJ}4gPN|%TZ{ye$*MK2r=kBO;@*u62*j`E$!lG!cr`LtQjq$}rK zm+w*}J5Pi9M5FTj!hlXLgpw=uXAL6nZ0C<5`-f*fw|K`)zqQ&uW!gGVp|J~T1S*1TnWet0h{1(43RbtObVgVjQ&ABMt6d~ zm$hy`by~K3@s>>3rrvh180N0K!60;P(!!)cMieZ2PAaNK-@fr7p2sHS)2B~?foDe} zI>LzErjr>jFIXLYeSIL}nyhwVsgbyYf5jM1`;P9;YE@ItD^EUTcWjd_?xt(00nU0E zbjq!ndJr$fW8#t_l{BiP0JnN{d|XyuPJoAZw7*|W(m(u(M7L;ootV;hf9Mg+$e=9q z6Xs{^2%!*ghT9Sjmqb+8LIYNfaZfkV^q|EeYvJD}<|?A$OvJvCI;p0<#7u;Fr$;qEYNl2du@pQ&-EghE8w(# zD1bxq$EX-qF1Q7U!JmyodMOXbBdGl$)nSb1-dg+qeX-FGQ#SVLnE2)X&IYKVI60r% zPb12E68O?aHuN5n@I@wFAvQ_(4{%6oIJ}O`r5A)R4-W+0PWC8luoH5yn;5CoyW7}t z)9e-a`LicAwfuZhjGtco1bw>w5(CmKxN;WSU^A0FJ&)h9cR0wl7zC|qT$MWSLUwL$ z&{UDdk&Cd^I8l5=1WrJUF6;J`%dGmd{y7?n`inV}L^&1J*O75?`N|gfltI<*OWDwF z*uB~uvIsQkPMWE{{?=~jyY;)f^75jU>ES~ogv@^D*a1xY_6`nwH@((>_bnKg7@4am zD&DC!I9cEEoUGyErdugHBEBg{4PJggREY1~x#}DkNJvbC6DZ2_wfqV0IWAA0&4(-b zc(ccMDP=7#cWJ_wPRj6Wq53Bp(r0 zTakMF`}d`!9wQ+Yss1%XD|;TD4Gbhf-o&s=OZpR)+sDUHogR^Vn$pw?Ah>5Qz{A4B zLWQKDqN1XsLq#&)-ZMf+&-z!}HqkNg;FhT?C^SNY$sZ8~F4~WMX+$-PdrVJ1o6$U( z-I|&THa;{=OeJD;y3qucjA>KO4*?&}&(1I~PBBj}N)L|b*-1@|S8(sK{~j24PD>2$ z_iIGNLW{J@sVFl`!RXy;4tmyJb{i8*8)jc1r9LQ`oy`Cq_x*>oO8)Wcbl2Dz_T3<} zI3c$yckgp0U5Z&Fi{T zD|H+BX3I2JkZy3?4>q=ul0=26*F7jzs3x{&&OfOx)RZ35i%Nyxjd>dv=xbacC)%q%PfOnbli`ub{=TL)7Lj?cC?*dZWk z1nfv2;DeS52mMkUw^dw00gLT@P)=4mZ@rY3j_6n#9v*J;$CH+kF?6t?5OO~SIW3s- z^ouG?dXgd@$)Yq;N=ZpUCU&xrhpVOKW&)qX%a<=LEiJ)g3wy%xkc!UU8RFf+vY3Pf z9A~>CqDpf6w$)S>m$0x-&JYF$hFm;%*UStR!Wi%~h&wR?G1AD$#MoF%TU#g@8;V$; zP2Jh@{!+(-upgj?8!gmJ6AzB#wVMaauH$@jV5sm_g?zG*t{OcJjloQvhnSccSV8&u z&!=2w(DUye9UW15o~A$%hXP-2GjnyiX8@=in?Y-HTbp09aJo_@Tmi6M{x)O>B>4Vw ze+cKj@!?rTA==+5^LPDi)|~FH4uM!#``6D9>pOK~gw0b^m&17z?>ciYkGBLsIJ`Mk zJrZ=g0~n@aPLM#oRN7C0o`g~;yk9}5#G$S1{&Nf(ETz+m-Py*)cphtKd;)1%!J)yy zpJy=zV3iM{{vAOlpRd^thJ%lvKXZQkCClebE*1uFQ2yDkA0#*5X#SO1X_W80kPw^h z?9r(;wlX#SJuO(RshKd*DTnrYGVPay;KF=rS}M2oc@kH~eJpzr2F<6^&E%hNM_hM` z5x!uEVpgx=WZ|)C)@YizM>v}qr}&L8DQ9_3Qr{&Fi-q~LP}chaii0FgO7HLAk}0O* zgo~S%hrfQcL67zzOBD57$Q9wZb-v@_UL*T39CUC?PySknAfjrj%46lldX^fM2jT5I zDGHo)6#UsVSmbS8GO}iOEA6>17PG+e<0|ZNoSRyFL$og-l(MqAlAgFr!)4Ri&iTeV zvE;yErNf=f?xc;r+Iu<0Ny02{$GKnq7J8CnV!wWLYjG;b%egilE9WY8y1HUw-+!?l z8%re@8uYoBsvmGmwO0eZUwr6;*tz?oCk8WJ@`0+hUM9rTi^>6na5mQT(Yw4m;sV#qUIaxv_yfGW1nWVpGZGR>HSe$%s|Wg zwrTtgQ(E)aEXnZP+#f%Se=gcp;^76;ij7Z<^>NA)qQ!J@@$fu(Qqa4G6&Tn)D~jqJ z*fHxoI6M^CqK}-68Lh(D*(K>4*>RgG{7v4REiZK40G6vKGXHq>Eg}uoms)T^->tQ! zm}!x8GL`;M*xrFeQFNxoMM$l2MtJl4unv`6<77YatjM;d*CT`9O*98hIen0?n3?6M zpL|hHEB*Ns$#ac7!NAy9eWn+SlId;=v_v$s2W_FUsy`k1`iIwVkUiLK>8!9VN=Zra zn$g}&WO`Ci;j%M_Iglwy8*cQZ&h7PS^GMsLRMT8R3H0B>@xqMWkAvn9&U%sr0nPha zRJ3<|Y#c$g>R1&nelJ|qo32p za4gs&qoQB~z@y+NZepbjZR0&JM||6bGqhtM*#u^XuC6X0x&zQXn{<9vR9C-CPR>a3 zNj{N3Dk7r9sDs4I>$<AJ zOH23btIH2Kq%ey>kXr=p-$n}pn$9>T>pa}95NELa;E*yKHod+gu;%|~+W1m`i&etj z#bKt(O8UhM-d#An<^_FF9BWFvk$sI706iF!%-fh zqM#Cx%j9R}ixV35BwfBtw?B7#1$?K2y+hNp=T8L%)k66$b5v{31*CBxY+_t?#!G&H z_~Knpl3n%5X7ENI#OB!Xl0zb@FSVtm)%ZQUAaPdWN|o#h+nQK$)9@F=lu-q-3`JaR zjVZ%lIzQe-%jz@jPETfGW=6Je868z+B$1Ej=i}s6rDbPkU@k2#USy3ZXPR&s8XRW6 z-WkcyXST0&ANw;j;j)dtdvY7=R9sxVwN;$Tb2q8BHc^3kcCW(NIOBoALa4Efa7=WO zRonK`wU(iQl@=&lf%S!*5Y6evYlb)I~;MH36!(KBi;6MO?%+9Z;2W0+&Z(o0njhz}CbUfLh3YH9wR~XONVS~g879Yu=L;3pFfDDwTJ&7G)7O-=0Fp6=? z*X)|#tI*TCPZkvTLOfP&tWHR~lgazm(MY(kL0ubbZc&jU=<@H}fh4Kn_B*14P)@&n zqXv-Za(_0A-(@=iJScynp15p|+HH;&={5NadYp68(D+FqyIeyZZ)|LY{bI)J`m#0n z-qzL@i*}7FFm`>WatXY8*4A}K9nrGTlaH4^dGKH!8WaM`r%1|1VUjNtBf;%DoTpLk zvZD=AHr)a{n=|#otgK%G{YZVfzL>^Acacmb?D^EBI{~%`Dqh>!k{%&9#OWSLO$pH| zJuh9~y*oSITYy-QGMkw?%Q1HHwpXuS0d$8rI{;WBLpf(_a}%F}pVJfZcDGvB(h}D* z1&A3Qt8pj}+z_zlb$vx65h9cH^bB4YOj{TR4@o|O{H8OO11R9w@bLZ|6_!d(&%Jpu zG|?nMx9I3-5<aYfesLs}y=kiC|G9h~tnQOax(`Qm1F5t0Zg z8odq3SiX`*N_pZ2mX@DW=>wW>V!bDq`-Bfy-Of(1^|Kr&8{6F8Td;aR^*GP1ZFO3%}k!JJKKcI^#vS*Os$PUcS>46{K!O?Nnz7lqMOnh39 zGf++$OgQ*pBnv*K?wO}2&k4JzTG0F`M^^#|h7CqLd)?Ql<+d_Tas++1E~H$MC!zuY}u=sYz4;UGBpJtjV- zxv{2%hX0SEJ;k{NxJrtgR@T-S%w;Jlb|=%QewgsC4u;kp$^3mYBQwl+R8|!CY&@P` zGzD)cGpQY>H@~QHxfab+?sz&>|4x~8@k35!ZWi1!g0*hPSa{g|HhFm~g<=Le^|B4kG*n{!BC;k{ zFtciEx}WVEL2Z(dXp4-DJU@3W97Z3W7=0r(>w@fAGV1uM;fgV)qolZaa%u|jZtNR$ zj~+$G#6XW9M#~3732YH_yxph4oUo%n(;d?RmddcOo2nwIX6gXnp1B`u&enjO0XhS0 zY-~Z-Ll9Z^tjH(u?g1_y8ygF|Th9ud!wt_5Q@mdaBrCm?l0x=kYSu3jp@ynjZ9Df2 z>{EWezRa2x0EEiQ%Hnxu{!6wZI&>XzK2UQ8G>z4~>nO9D0G>VREARRTe*I3fZ?y4@ z5w3b?v~QnkzluOa$O|z&W|}Ir&VyI;EJfD^!&4DQOe$!89u)m&J@u9(pPJ1}$2%r0 zI#*vfH^s+1WMd7>q|eUJmrF6lzj5Q-d1)U$d5AL7>q~ujd2v*2cK{CS7@3EUWM%ia zrwm{eL6_;xJNbo_h!$kj7=r`o=Xtx=vahD**48g@EySRzG(2Bqh)X5JVOzHBinQdq zt$!{vF|Ha9csHjNLm0de#~ZjvO!Z%{*l5s&QcUAI<^~4`LEK^@A<=46Bv!7frgrDk zXA6rBz|uuke70MT$Xmub8OeT6e_qgf@XPDU$nBk2OP4Q7iUL_HtyI5!;^jP7koS94 zyx`#BVd^*b9a2n8jFP^&d3mVCRJ^@$aW75L%*?|#avu9d{t&UiyW4xx+J!*Hj1ZjE z{EUX8&eDS)vY=7`p3Zj{8cmo4cSi|ERVvAudt1MeHP+~IjN9@iK-SOFlR5{GMGI|F z*z!|y6k*1Zi-Z%$v~BP%-fpt>RVcco!zTLK!+Ts^SzYbJ0aH5e`i#xRb}cwGDYv(= zd>w;>Hi`Pwa{9K%+-9q}Hb}i{8)kZ3PNpx3#af&~^KuG6toK||22A0G>|v6Nn~R)! zlVr9efV7>4*#MX`{3qMoM?^%4QxvhjjO0Fk6K$+qSSn}WXzli2a6v=klb-3TR}rrn zP^D?>YlBGC?A8zOu&{&zrE)AjS$$oYTJxZx=j6{2W1N&AQ^cPe)>CUFxA1DYbY5K< zl9;9_{GPJ~Rv!)wSm6 z$4>1DG;Eg$gg-vTc)gd`t{Bv^VOW1LjB3SB8_Lk}0s{qci{)3yLtarEM8vIlycxJhrl+sg z1~Q=~-SmcZja`(tQQq2PqoYvUOuFK@U_^FwaXC3T`CC^3wzzJV{g;N_cSZh59?-<+ zdN^9cLQ+vxk*ifQ{_9t&k*B`{;obW>88ing<&XZ}P45=p=BM3=d}Q z_#XbICi$#i6BcWR9n7xX*pqbIYW&bIJzeZoL=BXA193XvEUYv8?B{*NLDcS)q10F) zr?s*=YC5?G?KxbO{nfcNt?HBL$jC*_4z+S?zqGXHVq)n2dbMsR>$6o2%{G1W=>pE% zcW+bG+s>(oAeX688pwRU>P}E=dO>GD_!K|9x9j&XZra4Ym$>44+V=Z?7^fE(>t6X( zi=R{IThwXp;8{LkZ?)6xoBXWzRXNAVGP_GXZ+P%^YW*MYvbc+)q|el|9H|bc(;iY; zncdNXECkqVhWZbz71lC0jjgQAY$O`XYp)b)DUB_aM ziHL}gai=%x&b<>7a@=|Klj-@Ng}q0$g4;P_*Hb0JQeB)K7+KR*9xhw0R8?)x9&QM- zVPZo)>wKjw;vnNfykgCKYOG(Aoy1R>oScq2>M!L?4LMzo16o^K(M0WeD^zJyZ+;Ie z8t{%Sg!oyun+}yV$JRW))XT=4m84e2rkeVA6L9k^+gCDo%kd{dibr79_a(iI zFaG|m=l&S-1TMH@!JpsLNZ<>2FmJ7uJa7}J{uEOX7aNy9u`3vqs-7rEPfRYw7v^%= zig=f-to-H{e`3iej&?L@4K-B=Cd`Gcv4xymg&~TCyo2ra@ZjS7CSyc9vsOFt?%SE` zpRpN_eW{_x=tQIDF3y(AwS%@?rj=?XO@ z@|q9N=At?`zve3{T-IeE^pJ5;4y`OL-#<&yWG*NuaB*}*qeRhUhM?VQ7%&fZcgcj@ zE9zV@{q>|{n5RH7+~9*M@tI7|$|^A~uF7hHdslakSIBO@d31F2@bC~~l3B+z4|tEY~XMEp=mvViRv2%283tRfWd&YCQzAK?$$3%(55VO$;(<&u0Ay z7#SHeze{7j*=Ft;7Y&MLS;H5we*tsR-GpVbqgCcgR zBL-k+hlSSQvx5~s{HC4T{|u7X%97!R&WVE8SC|~l`g_q#^`N-ThVz6(bj=y&aL{Gh z@7P)y__AWBi2PnTX!N}>N$#2`F_{sFpkqRqBY&NSLh9gf0E1SdGG_Gty}S3$P<(oS z)ARE)@bJX9Ex}2rgg4ZQKUIpEtsNMPanNTeXPL!COVU22BrxILBE&kS_8s~2=SJJ! zicybAH^TOC6^$7Z``%||-Qn=EFu2=p9bN4_b$hnkuAXRe>GbsyNVU~Lf_gtIwFF{eU(}lrK_tp z#NcafV&au{oh1ZFqKUHOFVc%C8fRqOOl)G-se?rVa=MNTS#~mHe18KnV?FET#l^P^ z##g&3e%@k>^XWPN@aTzxI0nt!&^J^$J}l9hnZ`eM#+u&OO%RMxR8%uFv_vc0MZUZ* z)j!bWv8eH!fI@E0wn8`mqn@gw;-0^(u(*VHXClX|dLWbVp+*OU-q_l-yopxnvg4za zg)#?wn*L=56&tS3{t*g?IFUmolZ%8Y?a!jJDwpf~g}0^Ci!*zIs$RV3xVLy^jFIE@ zK%O9UGogk~KIxsC+XJlqUB#3a4>0svFOKP=B86rOoui71Zu(MxkWa!wf2N_av5=LC zPa#xhcj>|u^O+h1v-G|n#U2;{Dg9J*cb_Y?x+QJu-(w|fW!&6a`~f8~!@a$>9QD-v4EwcN5!m5|z6nKs z?EyXMMv*G7`T0lheUI_0Awk?h&K+3W1eG}g(LXQ%UEi~3>PeEjcrbNbg9p$UpcMC0 z+iyXQ<>`>>38f6Qb2-X+>>L(neeS!nK606;p;Iuw!Hk-gnwr5jS>~0B|PcE>|{WCguhIv~ngBB?Q+mbfy3B0R-VPKZe#lE{>&W z!@;_Ku+k%){@K+wzU3X;COM{40NHaZeqLU9jR^^w0EowB zI5^n9(@ctwe-hBNcevZja))d-89250cv3Pl5yIx9dpxi$HDHi{KtC%h3v}}#RD4P) zrggE&EQGsOxmL}%=fOW&6pFScT$r`aWs~bXjyK1VQL44YXbUfpTzYxM6dA_ zEB_#JjC658GOTQY6)r+ZYd83N$byGar;?wU`MA+fBD3dv@Er;pJ-z8c+7a_oe!)x~ zAp81Ez4b<0$Kz-Nzc12wTpS)B?LAZEWaL~lH!jP{qNS}Eu*t`f;kB{?=uD-rU1^Ti&!=^D%j+#~TkjFpf7MVkF#y z=|tSK@nT1ojfZ2v^ry^;V=j~H>Z}hS__ayE(hEvaulu zq|=VqsnGy|Feau8e@yiKm?y5y&K$T-!={IHbKq?brIgpIUkkWtgO`()B^c)FI_vE@ zEc0^#_@wAO)mr1JR`C1_F}S<$FOFSZUVMC!x?9*g7-6DZFvaV*i341fMF@SwQS1nrMw4-e|#`g}gGw=2m^h9L#K&o7yAb0EVFoswkM@cT2cglF0SLes6#1+~0p%&~5j~P-JA4`Ei?4fw6J06gm3JdI61BD;PfL zQxtfOJHNs`2C(SF=xB3m>jNzNwf=NiX3%m!q^0$?c^uHB=S3<)y&d&LMc=>xjNOov z+IcPqzHC_lAOZ8~h+!!&DuQFMkcIj+nahE;feH%C9H=KCkx^o|M|JRUn-1lXr`O}1 z8@rp(M4>^2WL0ZGh@n!$b3Kx>haaG4r4)2^ba#I$rl`rxBP=Y;%p8*X0|Lu$y6@|M z{)*-b-6}w?uwp?nd0hhtExZ7rtpvcV?~RN~e4_=pYzuR9=%$+%_8Qi+uKKB0Z?75x zs2lnC6$UO@q4gA4Y6VghuxL3E%%Xos!hIHuT)UXa$nFHL>XekaSDD`iGJ9z09y;)n ze@H#sDsO|>7?8SST5b*h8e4$_z0-4TeMPy z?-TR3Hpe(_iMQ-bR$7b|FF_JOdV0D_NikT!NXd!OG)D;^#orX`BgYpqe~nmX(ok`~ zuzKV4MoMaJZNNmLB^39DxuPRg&GmFFgnuCRHv$MKh2HHSt`Bv>yo-+Rr2o9OfwNQB zM$aR&{^#N69-(A9cofZ4ag|p=5V3ZRKFJ8V#UF6 zASE?n&NQMVBb=QP+*x@Z{#sJ<3v5i~F{Hm2bE)jQ-jiq??@U}+y0%sx z;)seS>Q{;Pae6?6E|_>LHMigIrAgpYW!t<&Uszk4D_hMugZcQSuvcLF7Q>n2_NFyk zUnk;@1AM;2ueoh)5EU;zq>hE}MaOWpzjIG22<-_*AYxkS(3%ves92<>KZOc?t%E~K zLHm(LPt?cfzTa&3lrl6>?=`y9jojR91_oy2o81hhtq>zJG6GwHOP+XqwC7d#lpCN# z4lC}98yj85RT$+BT|;W7w1!+T=Z}#lcTd&2c z$UwU0Y!@VOKZSaMmz>I`fMwYuJi*=Yy74)E5QI3(K>F?A;9*P2c|Tn&trQQB3VsZZ z90mE{+CXIq0To}F)d(eX0Ec0(H(TJ|H-bQH8U@_0Ki%!^?WM&(6Hc}@Ei5ejh8_9y z0;=Ohs7=lA#ISv7CBt3c-LjdgTG-ket2QB6TuKvBgdjVo*D+XMZpNjW+LjAGoiwNt zhme~Fve}WSKY#u}qX5d5I6(Zz)pcorLNql*5?ui-#2%wCwp1z0(%Kp#$$|gC#l>|5 zddA2WzF=dImD^Cg9hXZMvNAKn=B`OMb3NN%f?%!b>FKE|CmdYd&p|=EnaNG=36 z8UhjmZYCzAJU$BUvm!*Y13f`Ot-)Rs!`*$Y!$S1Djyuz&2RZV>@g`Oavnw<0eeFZt z(|^BqPBP@1*u=_+4>&3**}q+39_BuFK9(cMOcQ@dN2gQs7KY70Xz!?hd@C&61!V;k zGjjY)hm~$vJP*RKEU)l?MP!k z=hc+){c9CiEPbA$0G!jTB!b}!?c)4oh_WR59;_U2~)P&<4c3;l}fNm2LOIJ9ZSHO&B z4tik3&KF_;2ykSdc`0?==UV3P>HLp~?p#0Y3#V{wrLHa&)zfVpAh;OJIkI@@Cq5e9 zFK*VjB>?0m&)w={!Bw*3bi%;+y+6s(2_*il!S&giJL)|Jhww;;C8C}-$E1U76278E#3S8iy zu~yp2*DT1m{i62_rwwS_#$IDKP^H!GhLS!*l-F@$pkIZz<@-7FTW`7apx2ZAyK1j~ zAZ?TRE%Fz9wOARla8lUsNj!I&lke>HE?qEg%uB!4n%msu^cb(cl8HRZ>Cw`;{C=H) zy{N-Awtd>0TslZKSrK zn!w-HL((IfMUO8gN6`EI!m#nN2)cSaTI)NOyXX4Jm-yavgEdR%al({pC{vo(gJaE2 z-qms$!uO3vWp%cfzk_CKP5$$eeGu5l2mycILap4!fGta(KFEqLczeu^=UVVJ<~!;w zqj*7gSw#y@C<0|=+~Tfhrl4pMey>8>;PnVQTId}6hq_IK#h0C=E!lVsJ=>pMvi(Oq z{2Zs`7h9f77y<-$97S}T%Uo_an`Geg>)JlJbSS;z;#_^cQH7EDZ;Zyie{a#pXA^(? z5!F>2Lm$*IA6|c{vd}u!d5n+lUMCQNi=C|lH{^1Go#`2l7y4E9%zQm!^Lm8ByZ-yX zZ-V;T(0hb3p+uCrfa|)RMEAdTl||Xv8DbnfpPHhE0=9p3yZ`*xG}bqy|3rBFBha~r zOx*CFzx4OpI~s2kvVR?F|2)VBO#$iozexW-YaB=+7akOP@B9Dzr}xE?&{_ZAypaFjlb`WCAK(;I0J3)WHx_q?M}M zRzdbb^voirDE@1*7tzL!Yn*)^nc}BTLEI~hcDJptzCj2r?tCib@*&;&H?X(09Iq^` z(EF#jez+Nzfs66I_EjNMxr<~y1xPo(2M=K?lphHiZLH_G`ZW#;A68FU92A)qbk#2+ zdm|!z{(B)8ZJzh=2F~*uvr=Gk*b|Gfy<>Z_kAJ5zKd8KGyQc3C@zv$`sBQ2c_ zW&}7H@AA17gYEJ2OVPJu#J;KZ(~rYx8`FlW(1SAUa)U9!n0nc&Mix7@ZR*IXj3FQ5 zX*1#W!*p2&!>Vq6%e(&s5Ba~J?3a2>Y9Tyg#vB>k?=-wi2kWgy(DleAJndSt$9@*@;73+$Gl&Q|X}@+Xm8sz*R6#Km zlde-MUhRMY?x56$As^B^D=Zo0!j*QH4EHQHZqdn;lE%m-ALEC%xwUlIoP7Qu4~Bep zyI-oLCUmwj*Vk>0-}v9gM%k|jkjZ-O`jA6|k*MY9Bl;VqkW3g*J68_O)>|rB$xBL# z-dUUyGRL@`ypx_5QN2Xy4uHkZrV9Z-m;bSBF;5m(aS(=#G)qw1wm5?avM=YjQ zT8<;=9QAC&`hCqDx_1H*n)8ya%PZ|(pb(O&4F9n9`Pz6VhtwiJ zPX_+%4$z%|F5=GU(<2HUA4$}-lpVZJdSqY-x&rRwCqff-b=+y4J{+&AI{f?z`e;BJi2`}; z2CQSzok?NcwBgBF@gvJ(o~==Idu^mFI9^ZD_Q@sL9$hFpx;wKIL{yC1A%;ER80n zw83+wp!SB#+7addBkHZ=s@}S2Z&VOeq@`0pX^;j9X#tUxPU-G03F#0}KvG(yq`O2x zy1To(`<Z(i~SrwN=H>k=Z95eR$J4qN@v{1OQ!a0rA%79ld2k(&dlVucW*Kg?n?)3jpiRW-oL@J z!Gwg+f4jdw(wicH)Sa&zr$T!$BTRB+aM1pcSuh|lFoVWzt#7_p;5I$G0#cVk;aYjr z*(*pu@E|cEq2X#{li&ShfW3ecMIbVch0Y5mX?&zASqj|={Lg9E+ z7`VB4XAjI;tJmnDvpjncQTe)~XMP^)ZyN`bC_T8EUU= zu(9Bkl3eOu;9S?tiTIKq6-*TfB4;!&+t?jisj6w{;fu>VtoG;yqHS@0Pani=>c6Ng zsAtir!0YaOXk;Vqsn6QMpeO3|;PmjYzsOfl)xPv#wR`gXZu5d`D)&nsXck?bOjFP1 zkJawZC8@Eo;DGE(o+jaR>*Zei)VKvB$OMwAzd!E{0a_W^69PRwE2q^N&y!3PU(JSS z$E)A%&`1M%aX_p19LM7eSOh}h;@EgZ?&oIDkS~XW$$c=}89qj);!M>HR?5nvA?KK? zPtZUSdbUtntt)d!2ty!JMg79Vdmybc04;&nkkyy``U00x^Jw(5+I8NF^4u zauB}KfPPJhtGkqx*&Bzl&{E2fbl7!MxLw_VN^^a21n1P;oxPI-;5A}j3ovL^KsM~< z`T0*!F@1T%#>SQ<^+m_V{Z*rYrJ(mGZzOM31V6){+1bzhI5_VePo`k)yh0U;`tL&V zHxVH~4_2~ui6heMY$(N%9yORL(^OcVXDQ}1$s~H686L(Cn~&^H*5;+9=?%lQzT9nh zCwWF-M#sCJo9Eo7D8*JWyRO##bS9jrk&KPkcK#Rh<|dI*_s!WWg@YhW>*L+o-M#sY z%TqF9cAL3qY8^p34?g#kHtAT8!5n91&`vtV$|~SQ=;h`<$CikMPlbb%bH#@H6^8F! zXlpAPDWBw|_9U~LE_@v{US&SBgV;47W8Mo;92t6p@q4122xvdx$r#U5>19l;EYxV! z8_eESVbE~`K_rrnmmwspB$cJMEcYZF?=Mi_ZW9i2@G)#T7nnUcri#em%qB5(1dWc7 z@MqJ6mHjFp#z=K?dqZLnm}G8_I+YW$1>*&t;LKFJ>y)OuwjvLjB)~Kh4i;&iSaofA zJxfE5_h)@?lkXlSieW76MJ@Jrb{f4&ec1I!A~Lxu;VZyOEb|MnB*KsO_tQ-i$b&Ly z+YYUk%-IRyfYuX1c7AvyD=p}K^_$k-zO=56i24t8YFDxrP`vC<8*+Ew9@sdG$R9Pg z{K{NvDR(>;PWz@Yx(23p==vqU1*{%A82q~*E*~dyx$%%GO^EfoRcp43*+pAfU^N3hDVezW zCv@(dc8HZPu6Tal!uR?7dEk}r%YWF($=c_a2EN#}wKe%SPT);z`6#VowS@QK=7ala zhI{4Cbb3L4zSo(p{X56r!uBiz<90IfRc_yWlPCh?RG46qKcF}?YEuv0KuDI-G-HS+%9 zp%?}g3@i2*g+b8%^$SQR;BQ{xzfF1#Dj`)bFQMI;dYb6pN59;Vgy*-^*d)S3W`FAG zyR{;*Ah*~iQMH#Hp~A#qywqr70*<~aVxENP#26JYp{}jHj;7L|$+RH`n0T$%qLGr) zQdzEIwPp{raupS2lcl9~iao{)CY|Z>e$03}+rdo*6VcUrVrblc#mFED9sQBHJ3Icp z@3G&wQkAo32j=iu)AMXRl0NYd5i-Sf29V`(5Qd9)Hfv}Lx*Z{kd3%lJ7Mpb1DYQ0maXy$t6yl?&t4?t2h6}2>g8{UE zJdtCzx3}KEPe0Ns2ONuvN3D3?AR7F`io%_}{>9}dU&wlI`lhBBU@G+MPbcn*QFF6U zEbdBv)gE{q+Lqq;7>`T6E>OVRAR)2;VIw-}6+iX|lMKOMZw?)+eweS=u=7>Tj z>~@Fgs`~?hl9IvD@g~oRvNEF39Jls6tqf)Q zWGzh$M2L9-r;UJsfNsd{++5S@A>l#PyX9bS73=WeW^@K!)*Ea*2f3M7k(&6 zN~brcmjm!p47mdo^{@8o~toL+L2*&Gn@*5M>78 z4*llMZ`o8nb8B{!he9u?isam%N?5)Ri^j3ri!KSp8}0NNj9_M0S9@ovr&lpg-1KfNmoewm1wDV9?0GuoQZ(lat!Wy__6;@guT# z^%0^lX(U|9Qr$MXz(tW=$A&5NDQItxfKBkNbmLx6<^$ohe(#m8tr`AoLw|mLJVrG* zcUn?xr{Su})}wQDyidyhlMMe&)N4dsk`t2GaCS@Md4hq^r-ILEL8+)}2k!s)As3gI zUdMloh0|CH-M6-GJ-kkDuH)lrfYcI_B9Qp(OGm7CDav1IbMrhHrOhcianIYwJ-&w6aB{^GKRHN}7 zVCJHKic%u>+-8r{8UJo5c|p!8m6%>*_qUV8$yS>c%^DC=M~+#&`rKh4L-%E)z4R$@ z+k@z-O2OQpd+l|-s|Qt@A?aU#577u=)t-{VSKzNHN3G2ZwHFa-DbR2R1v0c1k2kpz z4eY|!!}2A?<$+SX4izHou-Rflc$h{($4L(H&-J(>iCkID}A;J_Y`5Kkl zIh5e*z(h1vrg`l%yR`rKR-V(K$oi*Km`R? zjXCCDqT&9+zT^fntemTC+`Er-OpGp{`c1;H)q$4;xs8Vy4MlQ!--VatPBhk?@>fw* zt@F^uggsK9p_nFQ&y|mO)x=S~T;0;#JXG%tCgFkx3}v7Ww^&aUo|)lBXdnLm&D4Ko zjs{sGf#Y?|ftaxFJ#3bLW3}h&eNjs-pdLXK+B?5;ekO}>Q%2*vp@T^|{OXWKQvS}3 zSCj2THw*~=5R9R|z%it@k#=qE(7L#CwJ&9T%7?mjXkci#KizR_SEY&CV&r+`1tp_M zpyCD*(Xt&<#3CqAv)AG3_sKtOnm;J>HE|m_hQ5Cf*&0w|z%?)DWr`c*;NAG$&PJWp z3#f!L-sIZ6JUN<7nvNl*_jE7%P0B59eOk`1FVQ_b>;7F1ge4(v0mLZP-nS4UW1|?8 z^dfy@ENP~31S#VFw!6A6O|}wUa*{7NAd@`SOp2;Vm6c<+)*OFY%1PAXmV2Z8orXD2 z-q&$Yw?AJKq(1!RVq?49iis3!ZG{gX#g~ejjOUZX+Qo4qRPxPFbnz=(J0sxKWx6_m zt0G%Mwr5>^jl+Z45>FLVwXaHa_3pcO0@7lYl~H_$hDQh4uXmOz7u|k8MOj!gX}q0F zYl}?1k9bznvAk97kQT*Kp@EvHo}Za{1jkCuvoI0)&->jw9@O+NUr=b%;9OBZ=%Tn~ z*TpsYU$YPTHQzMkaFQG6IzeUVc{GpgcOT6qO@8tP)dv=6qE z@!`k2#G}@!=`@r+b0^!AWZg-YMn)6)a!$cpC0mJEO*9iDT@8<$MWg*ipN>~pwn4KAy6u*hH(0h399ei%trlhIJ_aU7bCZ;SY?x)lwFQYc*=XZ8iLJ%DN(=W#Lyvw}@ z*drFfbeOA@kofh5@?b2ha9ZEkUsWcR4+o&e`}>o#99J^>akzrJyZeZ8J45JP$uk~% zh0pxC4S8(7LotuWO1F7Dsoxq4pNau74kD!tSjltHm^Iq5Z5!mdIHB{4R zI#v&jz0Pj-v2zX{S*fY1qY?1UnRGl*RK$KZ&8`RM;~>XFn)CR={x;bpI$FjDD0@hL zni?9;$7W8wDbtKy-KvsFiOyQNN}xw~-&?~BCto&PmSH_VCOR*kPESL)QMvnnd#itE z5o$Abe|-$~zHmMKsV9QVtc|#Yo!TaWfm4N1`JG%?LLMiw^WNm4P4!fI`U0)uY+hmh zOqH!?B^`?J&&l-xfoTkwnsr+xBUgrr`eh3*0K6C73F zAse8<$VTLt3R@~V6Au1|AKwiRli%TpSR4?~1yMzL`5i}ImhX41UBS!rMY4HE2tR-p z$Brb;(eCczxY3_j^lb}EQ$*K`d=#qYyLQek*<|KsASu>vKEbXslTG3FTK?e(DT%B5 zRkwKi=Zp*3V#{`}7av2TFq27$x75_UZg2eD(Mi!rxj*!-Hm(hHW)Xl`)$PDxAdM2v zx>+z<`m#qFV(Jl^x?h(Z>Y`D~?y!z06!Y!O)OG4$arXM%p4 zFxZC?;mP*kBL`GS!cb3kWE^epZ&rFL|6>S0fR}rWOUiJ3)DAQP!pE0Ii2r{-_l|C~ zI;d=Z>^5u;=8S^$k;Q$o;-n0h0v%!Vs`eVYTwX7X)CXsUkB-R1WOm&WNN7uxRC&nM+tG)!dAmsq9Zd>b5yc2Cj zwn4BnsVFIfqx4DKX8}gK`@$$f7!)8k{beYNj!qgGm*{*5;iuq5GWpcv`hrzgRyUdo zyzemO9_ZCUL3{Yy_G*977>EYW0OFWWqrefn%m2@ir&AZ!$Np*jjaH$0fe|%!V9TB2 z5+HT1?D6nZdJ8%W#A)UZiYq2|1i?Vp2Eu<=@>`zCZgg z5QQSLGtD?A-rjlh(0u6U!$Fk$3rUbm3m(H02WMon^VV+P#Pfggnh|M?Tv09aKQDPc zA|fBJPcLd-APGc{o+v9o3DdgtgT07(aC@8vhJ>&$_KXil@EYyQ&~SCF_vFeqXy+ueKDr=+i?pgvvS zwngzZNNW7vZ^F;d*R(B1lfbN5hlSZc-rk?$i9^Ou$3RaH;svSH8C#1*upCNCHv%lc zx?6OpMM?FR_5V$i&UNk?4MV3*v%y2mz#y(W(VLTt?VhQlX~N0=zU|LSS!r2jTF;%W zB^UN(RX8N&Y;tfRL*=D@s|0XMbh<9L>QA%R%Xo1_z(2e10=Ie`nXF9WM`&mOcmN2* z4^=9_w;B&xmESkP=cH35|Cg@_JWj}#CM`dL%Ja2o8=9jvl6a_akX)Q|u2l?#c?+ zBP&-omk;&c@JT4qAth9(2XkcZ@U)|lK$TzhA{cTobiC@R z7}zsQN;XX8`@7I{pmhs%8I;#6{u!p~&tf>Zh_Zu-SY4WuUplK4suT6|Xwt=it7~ri zf&SE#W)lD}SgEno3eaT+JxHxDhMb}5J^P~)diUzefp;5Bo^|znZQ(MA>6OLuKRgMn z-wZk|e2;S85r7o$sJD58)>0Y66=CS(;%i>^rC3zsR-R7R+Ak{a$4D6>$Dm)HxtyQ7 zmHPZiphKGc4JDiQd%^v%CnB}ROe_h2Kghf4= zK-(HL-HXjOg$uEP0b-@lf3ttYgTQa5>RAd8KK}Afl^4qcLm98W{^Bg-g9EDR22bOG zC1(v$ln}#AF&oz9j&bwd-IgWz1dnd4(F>n6@Lw69<6v|l9Fbpiif7t z_`Ld%X3|ltZ`_-em6JMKZI2h%d2Tga3D7H=#VuZkwLRv>3tq@YV`8#e4l#Ts?94YV z(vQ17>2o0_|2Y#F5PGU6LBYYn^JOvF;1G$o8OOC8tTm4OHyxN9_qldQx-0bl7hKSv zbayZ%w>+=sA6fY0B=GrTme^}KjMQGr2Lm%ROe{@gEiBBk=->@nSur9J`rYb~%x7Dt z%FxQf0CT}MbK3QV4R)(H>B$A*F)D5U%v#AUB7fe?i-=H0c5aVWo>mnHYobXM$Y;<@D8KPw=lwd&NN098?ARusmd7bC``N@(6|B0oxQ#^Jv3Yk)U)DQk z=HwNa5N|&r+{Z)#e+VuvuD2Tz)&CjHyRre;xS?TYYU;|F-%jV{s~XC0N4)pPGInOY zf131TA&Nn{Vp>iUU~7{vPj|V|c@82WKqvqxp8ubT2#w>$-x(*i z3WON1M=zEiZUBTgJRVE<4iPmTmD7-eC?a|lx#_z=29%b{d`)Z|D`naH2 zg z!u^U7LITH}pii{4q!BJWb{@psTk<$S4gLG_kH50SWP-kY10Skqnvaj?yNry`y!HV? z)~Qf)CaWlK^i*tFx(5QtNOzxzVfczpS=f?EpyjzsoBjPE&N>ii+=tER!%~2mtXa)z zgujMxR+*kscP@&z-dP&0OZ9PzapZ}LB9)l?*kZGlrr)Np{8B5nmKr0$Mzdo@tIu!7 zqUXC`FzlXKzV{b9xA`MZ(~aU5R^5u6C0ZK5$bGZ9-91W1b!^6r9i;ejP>ip6b=5|K1I@b=PXzD}|LO z8=R@~vBOcr;X|=JSmYmir-z>D$lKb8W*OFH%ZsLExBYn_Ad55HnH!iTs@`z2f}I@t zm~_2mFs|xcHdyB^ed=g*uX(%UZsJ({J`G{B&c^mN-Z1U>hKSC=XF|8`s1<4FvWFbu zazVNHB>nID2K2;FeIxii-ZD&Nn0!`b3nY4}v4|g{S(kB5Tb`9isMYz=Wnx|@4AXUN zo+c!zp>W(uS*`z-sbf$v#Xx*TWnd%*rk~Tu>}2?_bNLL^hsMNyyu`cDQRT3>;hKsJNZ+I-0RxH$L7e zr?zNjF`D7#5&EX}t|3RzNeat$9sfMu;TeByt8Kn;!0YIEHf2uB%&uiIoM0DBYC=g# z;YddNt)kV8BgE(AJR<%hnPn;NOWtUr-@cw7yddcM&Q53?Z)-HR5`8jcH+X2+ueWsD zU|%%dsU1zJ!7kE2PS2>eePNg5e1$UpT}d6|`pF#9dh)TA_A56IpVeFE$v-N}HTh`! zg`G;(?MFYfwz65BT%%TtxJ)N%KEfX%Votk%;iZhYu*h@P_-OqHZP!yJ7g?*5tkOV3 zV-jSs)-3n}7i$@1Wo6(4HcoL}iIVsS=_25<%cPQIzs?}@^FkMKr>ja3TYGU`!^-Z0 z#u%d)uS;=_#;vF1T4(zD+6?Pr&qalRd-caG>BYTW=b&_i|NXZbRf>&(jVQ0Eq4G|P zsD_SQ=o9g3XLmd43qJck1pQ^EqHgMG7BSn^q=wu~L}BmCztsi5xQz4bibYH|FtV3B zixR%SLYZb!lUMKEEm~a(kEyL2kkH^m^hv3I$Htk#jEeoI0kcOt^)sPJ_#=Y(u)Fut zGJgG)C{&Tga&xXHGG@wpH4(emf0@Wf6`Z1(nf&<)>c(Ay=_ZAU&_MIqyUk2T{19I7ziMowrhoMDCuiOjHS z1!J(TeV^tKD1Ent)ZQ(Hx7vXfK6D&+wkuCT2_IxOsXWGY+GSIF6L;k~h{4qgMgUbZ ze|-=5AM{0atZ>4g&baR`$LToLP6Kmn9;nW3;Y5MYH@%MfB|sLWnEj@CeHKhJU{(gh z5(KqJ5U|~+P}0$9v|VfkAvvQ)1^oWnnn~rPXUk)*xjATBfK_apYuZ6kNlA%iIh5;u z(`*ctn!0+8>EN@_KdcsGKl6$jcv)GoD9ZF&?&l4w%gOyT8>UO_=CoV-YhF~($!!1u zeJ&uOXXu2R&g8P)@zD{3X4TIhKgV(J?Xmw4ieXTi?doT2N?c?Lr7WkeSHu-~VqF&DGgw-_zwQ9Nw{k#QI5z$coFB z#D=O~vM2TeOtdfN#~TH6M>mD(N|E{91o3SKW^!-1=IfUyTW)DJMjiSR)+XfB00&W$mgk=nT9k< z{lBFJl}Muye^@R5P(Qe`L36KHNOzAhu*iBv!Gn}YNw7PhB6396t3sXajRWl z0bp}@G$`BF)<)@H*l@7{>T~BAS7V^w0GZt9@x^T^Ov*K^>mB=MBv2T28m`E0&mQD=f@`HyPZFLg4q`^}coYC08!ez6LjQD<~DI zsQ7Nq77GdrpeY6RXa+{c>o&og7mgcB#0aB9Lwv9=gNq_0BI0Hx(VEozYJY8Q4VDa; zqzbj`-Ns7{i;Ihi+xXAs{J^|P%fOJIo2%B~aSm{?8IQwm@Qs461cdgr6%|fLYyHrV zQLVOH2G1rND=U~d97eUADg0U4*dY4x3Le7x!=9J$UK+bly!hY#)Wq!I#M-1?G-^~T zCW?Q-FB@%8nY9|q+*os6F&oe|b|mq|sdC9sEZjF}AO_I~QQpq=8q+Q*jXBuDKGf%X ziS@Msfy`f%rM)xC!U`)P>gTXZ>K&89DBO}4vRP*pxy-S`K9ft)@yIK5j%!BKTr6&& zFuD#?RVPu<4BYlDUQ!6OrrFu&G|7o(sJUF|=cvC`)mm`QYG|Czmkh~9x%<`qkKf~~ zCBCo9>*#-CGIE<5CU4eGYp*TVF_~Tl_q^R9`f$DDCR-$3XIWw9{G{{I&0iwxc+sev zgvPpoWt-69f?q+18_scbwgF$sqy8{8)O2jW|MSC0?+tIMNsV35o6NSw;&BT#HMO)s zHB4bZzfx0ETUBa2ixCqS2Vv0_oe+k2hCI-M{6VgEzTZa145j$SP%cW^pHRHUqXB9A zl`atV12zoYc(9`t^_u{d=VGh0w*t&^uuQ=F0}>_;t7@^L(Z%*Mih@=2zeqx=P+HZw z9%_5+i~0A#Y;w2ZatC@hJ*odNk90U8f>-nC{}x=^V)f0%W+VJRdwX+=7O3AIO$|$_ zGnR31^B8P#{}!*RD!%Y*rZ@?4@%w@Ni-C64#1v&t+Y^AfXoSxjB@O%zI?mIMiJX=f z-PT^E0GEX2B>>CPdCC@9cn!8-ukG^Dy@R3Ki_u2#JdNb5VUv+rmyWOWrZyV1M~`Z| zTLM`fCUIa&0?+^uh-!~VG+<3>%E{T7-Ta@mgwXT3loM3d!q+Kyh(fta7EBYdafxh& zHgDFZq?w=F3hPRzy=tnhiTAwJh*K7e4hjk<*J;#{AodoSatdvFq)-)map>6bn*Vr4 z+*!VQchYl<#9k>qXo*#6`YEx+9Hx&dnN-9xjm}K=uiVsLN|MNpkHYodmqhAGkByUsT@?&^>t8C$B*Myz4+gjO}?)5H?lv0NHo+ zzk0VHR$U_&4Oj_C{fWa~FWd37+4A**3?gM!gll1y8%^*Z+YWH|a0I!!R%a)j2ywu!Q1?71z~G zS#UZ#J9jQ0z&xHf4|Xjb_w8~NiYx^ly#J z+2+UGeCV0{mixDbdg9+cslR|M1NDeOKAim+%n zfaO&*3aFFpeiPXC%*@R(g@He&U33a?u-c6ud9ayOT8#fHVc4=k`EL(ta$U`5^yT=C zBNdPx=iTuBY%h$ZUI63k8TugI0|Jq;>F921I3QH-Iod`fEoTn6k>A9~nKrJ!glBlUIFR?87DD=d7?DPbI8|jO=Fk@=P&td;HB=b6tijM zMQ#Xrda!TOqdLB%>21pO_MJtZZerKdHp&3;*|>Aoh4n5M!pNXlD(|C71KgC`uJOfB zJGJT@oPJSMLx$LbQ(G@K{x0F0&FU4_OUV5VYLPhKz^9|{pQW8AJn%lSz9w}*U^n_In`>x zhl~^AWp2C3O~9-QrYm4JxxyAaujrDZlB>ke&+p=DklOn^B%K@uwWTZh!nR)FrhTvP zzO+04T=wTnF=&C5FJ45gx)zOwV*de+2iWsq7a)UgY5$rx`K>&Tw3L+BY3=NM-}JH_ zkYY6oMiQ-R+ATPXRErn<&;?#gNnK3udR6K+-;KYA`hP#xq)Fs>D#_Yu0oz|L$;w1Y z`m~a-X(>_FOVa#`te%v_tcBXg!>7vrtGUPegr?%{$vtDQ?S}WboQG6d`RN0yeRJz%rt6& z=?zBfrPM|l4foHoLEjYaw&WD0WS-dtRyHJ8j){eczGL(-LrvB_njQ5G#i4h0GoL&o z4a85QzGU?M$X3Yum4Sj{9)Pz@anO0(=aU^Qqml%x82A|hPi0h!#PwJh z{J|qB2&Xo;SiT$$egO1NP0h@H%{$njX95nWale%))$?Q=bojvBq=@T8?uq=ZXyPCk&lwDCb=v>9`;bGW9BW7$ zqJU_^PAG&y>N4je7K#hZC(rBSQ8=BLJ4@ge;?L9czvDkir)Oo-nRvq=XsVmXMwvM} zwR0V0mQ#dhyq=%mVD)pk*9|lBMd+HVBgu?He==-~7WtWU zHIjmD!6Yk;rf}Yk_^Hsh>p-436{+yFi;>!=S7MS_3rM%4KIZSEM0Iq78&{qU((x4M zw2qE%GFuvF^|qzOvxGeD4^T)Y|3+{w$UtLR9NO!(!S{Z_+d0>6UP(;XmC>7)a&WDY zabU{*%f&tshwOAdL1OdR^gx~PIe%Q<4~_Fi21mZz>b%i)^hfjwJUMM-Y@5Rdru?^k zwGnYDyvaV9r)ke-wqur138u1g$%q%bQw72G=m@tfL?ISb5scqI-sPQuP|ln8NKb!CNl6LLUV3`)RaZ`Svvfg~5k5KynCb~AdlCTp3czTuVDtZd z_D$8hTLvF&lNO!aUq_?dPeSxrMWDZ(g&sK}bNrBuTnN7LDtn##>a%-NjT~5b9XbCF95}^d(UBC7MrNZgBg>(iAS7jEu?Y;$;odY!rpEtH@kXtWFu=HcYpz zpl7rhu%Jb$qWAMQpubp9g@TVn2?1ZQ&5R7InC3c0@L zHFJ4srp0|@YQ<=5kRJ-v&+uNQw26+#bWgR2TBRYvjm z8Rnb#fGtN!w8+<3qEhURhYri>>r!+WuOp=lF@#B!``4oe-hUU=%I%R%-%syAT1OSb z+{W$imLjk{y7y>kT=HhFF-v-_A{e{G;Ojp7#XiP+zTnZ7ShUJVrZ3o21jEAL8m~2t z8O7mc{Za6)66U81r{({3z5B8^-LzxMKpf**=%vuN199zWIS1}xt_9wo(WS~_3j;P2 z1QZUxsm3(l>t!=gCn)?4vi*wMNtvhk-ZNTWcA8JbwS`*B_mvdS3no9?;^<-O5&_z0 zVKtT&8zB!uUgf`hy}MxaT+;4fsU41E;KNVUIv0&w>}+iTxIA9KlQ#wTrQ6Y3MMZ^H ztrOtQ;X1$tMts1s|HHuQfnPwz+|*R9(aRHVhkAPRaQ}C%*S6+N+}zw`uH9nI=%*pU z1}p{8B%z;jkRHnPa9e+Xc~T%^Lv0Z;zK64`fQZupdKr!hu%{2=!uOf57vt z7OVAB_fBUC=*|FRKu1GkVr@MO#YBIGgmvvUXMr*-0swdwD$(W0Br0oZUBip1F~q{l z0ga#sSbS%D^KhF4X$UeJ@mQ6uJ}~6VN3}1T?xFXwYstw0`W3FtWw1>_)iep0E^;H>dZqRV$qj~9)6!&pGwlMT9Yt*#ot>KC8qr-<}PSH zkc5EW25-E}(uVBA4=?Qc49s6|%i^n4)kjO&PU)+@{gvT+DHBR^k(EkOR_1s)w%a)t zSUCLKKc-D1dcy#*_sdi^$qmC@_D$?+3AFbJZPC>;EGXvi0&& zqTIcq;L|o`=v>f>^5u>I>Q%xUG^}y33ON{@WS^|RbPwoe!TZU#9jj9`tDvH;_gi| zGt?ZZK7v>|oUH-Bnc+LP_c_x~Gnc$S>P$Q#NqVwL9`dxaWa>p4y~9oP`~7vMxXV&z zWo0Ghd1KWP=M6eE1a6gA_woOIfw?Vw2L)N1rEN(sId>nA6#9^-lF`VW^fPY8xx#64 z+$pbK7swlDq9?vw9XP8R>)R&Ur@gL-MG22VE{qDsD0xY&<8@q46;jPJPJJn*^LVM2FaP1&@94r(-voIE8IG!OP6W5cF@(H8& z&?B(A)MWGIo*^=s_tE@VJP2J%?t6V^*~{uvgt7SdVY_Nn3LHHYvX|LSx+v($EF4Gc zt44Wu5iWcv`u=@G2*!kO=^Cz2YDYEguK^1)(63kcKldychw)%WZgXkEX1$FIM# zg{OU{Vx#ZD_lmblzR{s;v#9Zfmfuv`O5`nIp6(l~0`M2>qR-I7+;(;v$s1kr3Yg-gT zaddLz;@~LF=VCWFKKBTh>E`EGjuvfh1=w%HY2+<45AFKcy7ujQvX;-_c;BF_wu$9S zGge1(e@&~(&Sl>2ZA64KG4lVFgEr#xQN78@TGow+g>RTHPaAboUm91?g!Eo5RoIN!PLq`)+nw)qWZj|fzrYOh$XRlPIyMtdy5OzE)M@vSeg3Nf)8*x{Gd zvAsx4J)W64!OwCXM1F)tlkBocg0UfP)l~?CCZwai=U+04m1%R4*IxgDuDpfK!Y{$+{B}9GQ zha-8$Ww3jlpyf1ni%4)jy~Gc)Bd^Scf2hUvBs=s26pz_$DXNVaO+v-_bjmf(da8<+ zU;npd=&s8`@N+QH8h0gmwvjmR^Fsw-YjPt=4b{YAI^sgMiGCJ@rIPfs`vS?q+d;A05ucwTpp=8ZDc zt=HhC-nNzH#t2?44Ms;_`;;fI19mL3?Ql(pv}!%TyxZt-!+PYtz@wkHm)(^f0<)f{ zGl#3lyV!{R6O+<|J|Dl`qjmP+Nys)g+jLFw-iivuopGseH@JEKM+JoU_JvdDeAR z3{Q*QLUjUpmstd*;KI!j7@q(;TUc0V4I?EX!oFPYC@QQs5}Nqg1v8NNNzohlm9_4kCsklsKC2UhfkJK^LuNxR;IbgHQ9xI|$@ zp`Gc<#?rjd#WSl=!^;RQM4>mycERK*vzBAro)bg#OsRnwHQOT^9cZ!E)5Z2zXTqty zaq$?pefPXUhE&bNlZD)okf4Dn{BD>79w>Bvo87Zt#V8bh6j#08u9_GZ3p2Wjdwi+! z__uS{-k&>{dB)XhLr>W`E6M#gBH2m#lx#0|yCQ3U*c*H z39OI(KaDP4U+>*IGLbTcS5;!<8fj6C|ykZlvQNpqLRLTC;HIFE`nTnQ~3*uS5&)NYCR60Cyd5%XyTu2)Os$?HG%?>L_}^#A_Au}r56?Jnveo&4Y-wp}_M4P0+lJGb z+ot!$Z<2YD$Xfr#!!#hI*U>dpPp#ElGP;d!M>~ro2*c5Gq+yn5{8$17EZ)X{PwK4V zCYK$GAy-v=>LT%@b>rnsOC+C={O7kh)U*T3PKzIlEI40kznR>2x?OXqgunaLpPjJV z+Pidbcy!idDY%h#$qoe7a&-GSL~uF9g&@(OMHDo=2cEB^Gs#hu^B^eNH~o8qLPR|w zZ#dPF$^eGOz~gJGf2+kyMpV8`mEpc0#TnCrO3S)K&GgRq@84s1@Sht&0MpT$zASKW z13!MWCAKb|6vta?tX={@qDSd?-CV-v=`Qm-x+jU8o}&X(c9$g1uJzhru45^q34zX@ z?0yeG$r!b2wqRpCD%pCcqJ2J|Lne50rlaGz{WzNHn&PYY^o&&kyX`N?U_m4KQFndv zl8%lZnuqx#+7XMS?nhC`q*X;aPDe=}HS$M|%uP(*$dY9bnq5AgX+`%G(2XiNru_?gr4@n;g%(_0#^$uggcRWoF zMK08NjKEz09nr*Y7=u2)TKs?O?lH3s6s=uTQ%NwQ1c>T`yQvA$Zs*H~sQ3M7LZD4< z&E%?oykU(Y1d(guBy8e2gAhoQNYxG-PFY0-si{d&P!Pyi>^h%T3u^;NTJDo9#&1Pg ze#q5+=Y96GAD_ptgBq~&tm62X5HQO#>rrYpU2_bY2=*3DkN$t1_2g}%vj;YN1lgmJ zOn=cmpB*7cMjNn>|Bb3{er~Ijln_nqW?*I}wRWdnG8lfH>A_U=M!aZrIkKZ=Hs>QT z8UFdysT|Iq%2`ji`P%LzsiCA5HL8v3?R0f^3$>k?^L{x+N8tWH7v2wS$gE9iqVmM? zo+J+Me&{qP4L4!+L@gJ2VM#{Y*d4oYplhKroT1?|TRySa5F)!TV&Z|zgzPQ2wqLa* zE2vRyH_WA{=LU$Um$gRSNlupw8iuI$0u_u1tL64g_rkofA74!Joo%jsZfi=T=hghs z=y$=fZh9ex+Bge!?1$8FDrle~%K!G3qLzHfREJYTk1lyZ30cSrSFN-^>181|F>ddr z;FVmoobJj`e9c}--U?%dwdPg4+j`XVR{bWkUXwZeT?*2GXLi1`=(blfT-sc=3ui5X zoBX7*X6jN_PEL*f7zlHk_5pWwuZTF0*mzKH??+Lsf0h;C*}Yq;s68)Ky5mbeCzvi+ zJLYA3d_Mdao~fql0CJQIZXc6IXNl{D;Xa9b+UfTh5t-v0AJRRJfJj|x9{9Cxp@N8D zQ_}OBSqMV0I5n)TtsNZ7G?~z+`C+1|hy%kF2rYrLu34A+BOas(IaF)HzPg$6BU%vY&uUzpS@_#f`D;!9W7elI#0o()W(sS<9M)QU%YS2hu$B z-hPY{5G9yPcrUfBF$iSWqXinPXbj72Egwx^Bu5mw#xGHj}{g=d_N5%*sn3rwD$y@_Y}U85<2Dl9N_ zD5y_cwD@HB|D)|KqpIw>uu(+1Q@Xpm8znZ~NTamUB@NP&($YvH-Q6W1-Q6YKEob>W z@B5xH&c8Fp_c0s}w`|z=z3#c@oNLZ&Uf1Mg00xK!#P7v#F%TDhSp1X$BZ$1LED*nd zkpO!Xl>aP7h=U|CJ!X`f%!D5b!MqB%CD53R%>_Xo5PZUGKj6<{(pi6%Q2tM)#cy(5 z0p;^6sc~a=`h-Eyp?*X`8vsRQ=}5x7=?^4mA+ra)o0*9I_JZjE07Dv0M@dNu+LV7= ziYxQ;$!Tf7*in>(W2p)7@Io-oJz8ySY(T+oeSN)iY+>8c%+wTounG4ixDcSgfyX_ExQq`bL~C_Hj}=zpZ%2(VvU33t8>cAHw$MpfTQmE#6%_P;s09r? z(8bZ$l)wN@7NCA7mnSDHJ7&rKUAyN+#?UtCZ#y;WgHjA2kmNr&aBP%~?E#RndEfx2 zr)k38J3BiAp#e)CzY8!tfIB>v_-i=e zi(j!Ygw$h?&L$vmG8ouRo*FE6!fWe*!1h7Ys=hwp1XGifznDzL#$u`BfQl7c+yJ1U zZZYYP>gjnCDQ&`;TxUH$F+Sc|WexPU(2^BsCqc{tmk)S#hRqiN&j?T}GdHJ|ENp4< z0z!uXoCo;L8+5$^ju@y4mKg$w0%%KtgEP1)Q7GS1Ulw6xNBY7~J0Y63_+0O9bmZFyN4$mschGPAL**4r5YMgcIegpVSfQwaig zcD5u&V19l+*g4>WlNDZhj04mYplEK(%cH4~_f!O*p{52LGhh=%_>M^>{gt|ruVdR0 zyaau`eP=;p0zh?nGFSN$JrQJ;vwM?Fx*OsWW630&L&H$Y`TVi}p3J{>RZUwMFD9;o<&N&XeciMY6) z9+B-FFdfkE14ES#OJGbJuvX|Kf*k>fhk-y7lIw=flML8*YuvHa1Dj`HzZtE%sb-G$ zJEZBBiZ0^5fbv5Iv&f)>oim9tdyC7~rLq)~UDc4Q= zUC#K=szZQE(PW&y+@A!JZl7Pm11?~oe@sP5slk}=w|)e4WiN!I?fz*>?tm63D3%zn zQ9}R?uWdb``u@Co2c|IqTn#KZihc)#Hg0=s%M@@C*VY2ndqBVOS4`5(thT-$4;L5c ze#*Rg1N_W}L^}YWQA2D2u{Y2Y0q8ccT)P(o+vhDUE#T>vY60xY%=kD!$W4Kl;k{Kr z8y0+lY-@dJy}}oa_W6_lO65~7pVijXJX&hx0(dPJmePU(I!4B@{CzNb4Bid8Wk9ml zp19t8I0abQ)RacjkR}jvA|etp8^p=jt+!qF0Idk{Hee5c1_Z$Ac6N7x8QaZQA|l>P4Gv&F3K#ci zv7U{GXL|DtoUs7QY!sOoKnnn!PN4jpUr?Z&%3GS7o7>j*SMC!CySTY>jD&#t956w0 zAt9imH6BZC10ZA|-~i_%mp8F)PmdD@X7`pxK=Tc_5V(LBzS1h-cndT?Ig`Q00ftD7 ziwiprs*0=?n7EbTl=A$~!2-+qZnEo(F&>DOK9I0rP6bZ|o8{ zQUC`;N;<+Gv25EPXCe^QjcKQ#5HYlk1=DHDDUd$$1)DC|kiMY=)GTpFynr$>*q*^7 zatYJ}>-P94aK!liy}hb9pa~gpku9Brc`_98mo0E=-x)kzw?#j&vH+gDCNTmU;Q%7R z|6|LY6rKIA#&~LEzE!03raiyEW*1c;h*^No=*mMx63n8#tPcheBOnk{5`+N&n;{vh zax*I$Yen*>D!lS|0&fJn#wrDkRe0~rEkb$|;2q9b^h&kDvcWzY`79r6%1wB-O9?tE zX9FPG0!_S%imyocJRdXwxeGahVEXaG$8s|r;7VWG$^N_$wiPUx1f#!}DT&lYz)Fp9 zC^W!EeD#VKpeTi33b3%CS_FV9$Fgm7WMuDA#LJVh5iBWikOT%ZlFS7!Y4q>HcUZaX zKpUqk?OxmgU)KBH3IKYTnR_G{MlYC$q-A6_dx54kD{{v;=1^7V(IJ0Sad9yfgp-cW z0?;V)rk1v~i2_7&=AvEoIoM4g(mr^#fjk?y18EQ`?z;ISF5QIPZaGAI*X=8qiEwd) zjA|E;VyV{=YiQ$uDlcwG3XDXd1|uZ40V<@M!+MULm@ZX;Hqp#mbakQjqeC)lQt@h; zl6iGlP2=dC-VxlT(#<`|aU zy^aC%VwE#$1<}9bQworWoSxPMQN_T31RI;nQ;kOMqOAOl!h z0valuoSgoX@4ZFIl&C@BxmFjn6aBDLQM%V)Yu>2#Oa|ES>H$GXH#dI8Nx+g-r&|gt zHBVYxuKdJ*xr_+S#LdljfJby_<7=2j8>+Ic7iy5jMe_rFYX=GtxnLx~Vw?)IAxN@2 z(n6|QTS@Hn4;>pZBpDNsGP@koMt6rsNBh6%6ANyHqG_XBd;#m3B&5**8N>lB9C*>fLIn8ASZa{!ypfUug-%jYFE;?~OwxCR$jKo=V!3!HmT6GLVj8{x32Nir;&r=N{Ybz8J8G5=H`vLSXTugM&c2 z)t{U^(3+R1-+`YV$V&>30w<9N6BrT#ywV!I66kSk-}aCxSl$DLB;1#)4Ty94RpL*%HDor7D_P z-qr>%jC;<4El`g3>+gdkfh@aHK{t;w(82<8IOcv1y+CAsa^FnP`K*PhDG)9t$n3p%1Ts9o4-SC3 z4uJCmeB}{BN4Uwsm4`pMTgE4TVmEdHwD$>eG=}xb`THv?hW_NTvChrQ2eaj3j#~($ z7r=Fc86t4oAj`bXc_D0`y2-DK<3Otuo12kA+G-drX4cU?i|C(4A7-YbPXjcRtLG(& zWTDOt zWDWCsfs87P9?2rwA#4uVY9vTStt#JwQunFf1xWBxpdzKga!Dkbo%L8ZF9X@Q)8+Lx zAQjBNRkwHqk_`}EaC0NUW>JuR{-?1DkO^^Id@zLNr@3lHK%gF={-jh<%C@Y*sYUh` z@LYhk9#{@=>9N#6%jxv*>Nk9-)de-#zrLEPNP{t;^DFRncAGSiSny8JZi#`4r3MmC zAmdnMlLZ4Z;I7a=_q{W%Km#Z8VLOMdrd8-brUlC3F5qMg;3y4WkdTmoY{REde2`fFmRha6ZO?yNTBPxVfXjR>{^}ct$>8818Zm#?xFs-2pw4Vh0zM6x zgtvFATG6keAy9n`DFNua-QDKp;{bT{jhIpO0;OI(Zuiy|AhG&r1>NreV%~l~VWc^L z--9_HCKeVc1mGcodIKEUzmqahPz8veWy&%hMLVb=NdA6&tVl{0B=5=Ad{)D=@dXiI ziD7#n66+9XO1fC;05(t<6b`6gbunPYQ;LNrqI+xJeh%Nt z1Btz75eE%I5Eb7!S(3a_1rhHM1I|WJuYYHGGv*rcz>6sv{Cm4p>Os;u~t-MWMd!43;%BqYz#kM81 zF0KPQtyl*)AKHiAf_*}(Q#yaIPr#7|7$Hf=1{mUwq^y@VgsKcEBfssr`~wDG@h2XL6c%yx8k35-qy!!zBs??w+Sr^w+?|NQP1 zEbu1&W*>7XW=QPpK^{Gl*kjR)Qw-0ozu#WdR>0|1uUZrX|32k<`~~$bo4$TggnfyG zdru^J5AUCkSd$b7RDaKpFD${kI@r$Qo}nSJZ&Wqjyy@vEIbr+tFEjys52|2vlnmPY z$46+@|4v8vO(JN0{_lKfUv!jYCXqg!^0VhUxg{{|o$p0CYEq zqnSrxfA3cYr#B-bBTfM8-~0Ac4CXSIgg-j{aS8kX?<&Il z-}r&MNb=A)0hE6)`tOb5{U86BwMTNu#C#bv=u}9)zWhRchYr=QMR)9d7BK^#MtyJn zuR*Ut(*RC42#ypc498_mHhnC$dslZjgv1I0PsmgVWNT7lRQ6(khJ zs@s_V4TksVrfXs@Hh1mTiP1B|_Yl)ySOx zJ~|8dzsdxe>?H}*U#KCfIIV`|x7D$7Fze`u{?F^57U|;Bhz0vyB(Yf63(_R9#CpW& zG9^$;>DuNe6;WV#7a6_su7o8+(RI=2(Za(- zdCLtt@w%(V{poA{kTG8!O_s;}m*Bpgr3LS;^KwRqB{2Ti`gCKz06`caRThB}Q3UhJ z#{z245HcVB4^#B5hR;*#%Ii~S5Tl>ZwXHJDWoJjF+hHFYT5I)fh(;Z;+_CJ2ec!zg z>!5jzSO2hZJ3*zty5B<%sgsE5ud+e-Zrj|DGqGzg!4QiBKt;udzg2 z;Q+I>V27>f+DA@gez@x9yaG|zvgIMPCx?JZD%{!I1s2LSZLh2$rlYcJm+P=jCE*Jn zs}pRo?>A>GTD%wVM7Lv;%F;$B@NFkmUly6X4OgwKIf?g3KWscdmgUSdU-uq;7M!X% zZy17ZWfQGlYW#i)!@)iT@%+%3$hyw$!p-UgW`8sPd~|YC+7t_uNcW=4fK0tc7kNW0 zlQj__*0YL!#&enKvx@G;-xK%G#@mJcZ-pf}z~QYYK#R4Xb|G__|H9|B#vi@lhwIzS z25J)sl1TIn42_3XL!eHmuBNsFN(SHz$ssNnES$*GJqo{Cy1s-1$zuEDX7qNQv1k)K zs5+8!Z@sQp2~Kb<$ykc(6e$~(Z%|3QzaUV5`MDFz8-I_(DOpT?)}bWKT5nhQyYhPm z|G?|4KQy6~oO->b+imQqMPD&{{NBnUHCm3XiaL8$hqO9o`%`}ji1d=e&IGpIPgzMe zeK9v}U}$BMtQ@P-(6@@UdSv0--or~}L~4@t=1d0nHO0$zhghxHs3*L0wf3+p&u~WA zzCB%%392DtZKK=+`|WDVncAlatLBnfDp zoEeHqQ9#}p5VaRXg-2)YnW)3Sl@?4Ml+#I2wx&=cUS8Rrm<**&N)Lc3=rJV}lC;Oy zC+IWebcf!7Uz6fU4R-Jo`X0~ZaAUttGuVo3dG6|s(ye+B)QhNG^u;(nsl3CFtquF#&r^F8Mi= zgTr{w2ef|zeNZR(GM=e@U&5f_(o9*ua@BHFU5}4A%T#`B@Cf)*^2a)s8q9@7O25vq zL=iFlH@=NTjajv#bdPZ>#!@Rzc77kvrdPuO0z<(Vx;2)n-ri3JpOyhWj6|FSn;1_q zb}Nf341zU|bN2J^V4@K2= zidjERs`bXvLtwFg_Ec8iCy2)sE>k|P?v5y>;xkhqw0EPipyFMfx}-ceD;CFM<9y*w zta{ejd|zmCGL%O)dojo5Pc72C)L4+1L+@J&mRMVuG$Wx0#wpnTUmzdH$djhKeqKUZ04lNK{i3Pu_l>8J%Vb2HKMpi-OAFB+zTyFC3IOcH$av$j z*wf6Wj`>;PWJBAgyv(uQ6}g38m4XSdJmst9P`A$=lL$65Ss<8KxFp}#PZ*De{E>wb zOW(htCjCyH1AjN);k|rv+Xg4#ih}8Q759YdcklCZigY{U;n)7VhrZOy;+1(hpDxO` z|2P~A8Oz-Jt68H|h5RA5b}uk<4%ol|7XcXB$+hxO1U*mJ=BM;eE6IFzhF)GR)DQqP z1~uhLOKy+||IL>fw!t~$@3eWMp6!?L@a|EVA-p#XB2iIO535_|eL3_Z?qD`zk;#j| zLGJlsiuaE(mE9)@!}wONb+VjN{FH;441Z1(bI}DyyfdUM?M*1&*v#ug%s7m)GqP7? z>ukf;xHq#-LCFecT)sWj_%1y(?wkR^%#1e)GfwYx9VcJRyvL*49rRD=R1KVGJS^O5 zI(n}U1Hk>*`DaNx_Tep|`i53R5=PBZrj{GkHW@KR3R;wkScOdqY<62DwhSzZY4J4b zKSRUJeUu|dvuHl*-d#P+@8@h8W=?J5=qSZ<{eiRh$SuWf%USkUm5Cx^m1mDtTa)G` zGX`F!KxV;clmVaHw!!+XwUOx3euAsUt!`5fI}ZnM93=d^PoX=xaYbWKtta@Ern-6U zK{mElYITR#@|u`}&n)_4I+eu>vr7!L_zWqOtRyTCj&z=r8fMOK$q|L;l>O-`OJi?8 z)B_?4b5lm_7AUd^D_N%0#jNG5j@2nIYB1f&`Vkrj(8aV;! zh1NH66$exqat2A6SF`rnf#;KZF-0h(X!2NpR@gqSrIRuUgf;PpsQwHMKwbLdVOUE3 zLobNObDrRi(~La4xp+Z){MW~Xo(7cp3Vz>e;@VI%87~tqGtJ@07&HnwWd#z3*&m@t zxNQarZF}tJU_Lrpoj8->K$}KAx*=z9b`t}evQXZUAboJvxfPIDBa|yIsO}frTs?pD zZW|V&O&^dyyYYD)VxP4#yBUu~SqPbrFjPD)-$k2p(xq`Igs89|rd2wkGc7D*^}}z7 zK+i8v)FFYV7>Y!N%$0nLfK=}~PQg1PuA19!CJT*=V&WKUwIqaAU(-7A{1Q8#qTigo zh@v%`N<1;HZSFWdo+=!X?xh%dk6F;UfC;V$+WPlT#GTzRm<*;pzf`NHJ?9Q+nI2nx z(_kxeJ9l(%o{08w>q*Xa(l;vJ-fs{mry^R0reZa(O_0wDQ_(1{lq%<6QF z*N$*Jt_X`vjlL^Cp4yhQ=YNe;17oQc%wur(J}Jl(PaB_L5WddH8KTYaQ!zR|En83& zeb6&d;J>@BCvDdASa%&BOgC(niHE6iq?3>734P-M60gOBrFBBJk=p|SE~D#P#gs5< zTZ1pr-&Ji=*Dk9jr3bHPZb`jID<5M{V~T2qSwybBZ;HRu;=zI+6-^74)f*h;(@pjbrm!(UKs;*+*zB z_ATm9^a^kizg8k*NEK9jrI0mUb56`g;No35(%V>d!{>QDV}C*SQ_A5hO$q?@xjXB8 zxAug+$>cdsI-X)Mdt=!k8MF5g@Hc)0e?*Lo_41_>Gs}G(KbV#isLTm7xKF2}NjZnz zQRMrC6rRuU$393G=bl@a-8jmx&H_sL)!X4nbJR50}(2eduHOr3> zP$M9i%9!JPW(+_~__^Q1v_ zYjyRyit5R8wPvvXgLT z_eyBKs6;fG(uZa7zA?OIKkLA4lUUAOz_`0;*-LcHk^lTwCwiAT;FHlB$vhKo+OI#y zqM8;i=|XEb65pB?l!JyuKt$i+hZF=*n(&A7J81`TFj&~ zIx%^IA@ydu?i)01SF5c0{OgI`;iPwkVmbFpD8jex)qxiM-W*}3i017QV%ciO7BHk$ zA1Hasw@rot7$^r6-W5u|r&oouu&u28o(viMvW*w|(Tv@%$&$OF(LV#>8e+7N?z`;) zAca6}1v;N(-k0Mq(S^x}&Vkb^UB0)*<}{gz-{I#-T^>J8jx`Ur8~f9 z(&~1$QB_st`{yDP_$E@QwPV0K0!S-HgfA3cp{1+BBCEuIr3riGl@dX-zh;~LWq*i_ zM&>epXO-0JoyTd)fUSJfA$xh|Z}&AuBa;Y2fq|#`A+Iom)hEhE=K!A*sWuBcq+hKp zdx`65d5BPcdQ!Hyui%2{%5Fx}UhzA%+)uM^E;?RhAh*_M?0=!zH`w(!X&gH!Suk5z zJNvZ3fhz2zzE-pf`^UTXx!L=+NXh}qZ`DMEco#lUXA(Ncb0`dDca`*Un4gE3s@x}* zc+n`sSH}L>rgKQy-uE)0&8pu0_{}W#>_az)$8XE+@sE#TrjV2_=Fb9c8D4yKR?&B3 znx6}7QG|V+$X8?ER@$dAaw528QFk?u+_bsroklse;JTRJ0kWd|Eh6t_*Uga&VJAHk+Mu2`LP|=N6uMkNQ##)j2dMVbcN^pN8{pge9xcVGtr$2^7^md* z{y$zH?XU!3P`fO&E8HJJj<1A&zWm&7!DhtpgO1oDphets%7HJtR3xF1!M@xldYMGL+ZEV$ zZ&me)?kta>Q)jXSbFN|N(Ye~E_sg+N`4tf@{{#Kid@8i7?>iq1jsvlJ{>n|iyN-mm z0$)70>-;R47t!q`%9a{95{c;Vkjlh*pEhhd#SHcWpu8GXL!i7WHJ=@e9d;YrcERIu zH4u;KNG*<4m=RQ(3P25nQi-SGKh}R)zus$R^vZ&&99e6M^uvsxg`7APsRvaU;

m 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 qp}}7`(EpJt{$G3@@Jax7)K~OLk - OpenMLDB vs Redis 内存占用量测试报告 <20240402_OpenmldbVsRedis.md> \ No newline at end of file + OpenMLDB vs Redis 内存占用量测试报告 <20240402_OpenmldbVsRedis.md> + + OpenMLDB 新功能介绍:特征签名,让 SQL 完成特征工程全流程 <20240523_OpenmldbFeatureSignatures.md> \ No newline at end of file From d9d15cd2675b9bc5710ce22f305dd3b78f6bf168 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Thu, 25 Jul 2024 18:20:45 +0800 Subject: [PATCH 38/41] refactor: fix compile for mcjit and improve to tests (#3952) * refactor: rm SQL_CASE_BASE_DIR * fix: compile on mcjit * feat: setup SqlCaseBaseDir for hybridse TODO: also setup for tests in src/ --- hybridse/.gitignore | 1 + hybridse/CMakeLists.txt | 6 ++++++ hybridse/include/case/sql_case.h | 3 +++ hybridse/src/case/sql_case.cc | 7 ++----- hybridse/src/case/test_cfg.h.in | 22 ++++++++++++++++++++++ hybridse/src/vm/jit.cc | 5 +++-- 6 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 hybridse/src/case/test_cfg.h.in diff --git a/hybridse/.gitignore b/hybridse/.gitignore index dda60ce8d59..707360df67d 100644 --- a/hybridse/.gitignore +++ b/hybridse/.gitignore @@ -14,6 +14,7 @@ thirdsrc src/fe_version.h src/hyhridse_version.h +src/case/test_cfg.h # ignore docgen style.xml diff --git a/hybridse/CMakeLists.txt b/hybridse/CMakeLists.txt index 6640c5b9cd5..6a9dfeaf0a9 100644 --- a/hybridse/CMakeLists.txt +++ b/hybridse/CMakeLists.txt @@ -125,6 +125,12 @@ configure_file( "${PROJECT_SOURCE_DIR}/src/version.h.in" "${PROJECT_SOURCE_DIR}/src/hybridse_version.h" ) + +configure_file( + "${PROJECT_SOURCE_DIR}/src/case/test_cfg.h.in" + "${PROJECT_SOURCE_DIR}/src/case/test_cfg.h" +) + if (DEFINED ENV{CI}) # suppress useless maven log (e.g download log) on CI environment set(MAVEN_FLAGS --batch-mode) diff --git a/hybridse/include/case/sql_case.h b/hybridse/include/case/sql_case.h index cb2d9907b37..bdc50aa8e35 100644 --- a/hybridse/include/case/sql_case.h +++ b/hybridse/include/case/sql_case.h @@ -221,6 +221,9 @@ class SqlCase { } static std::set HYBRIDSE_LEVEL(); + // Get the base directory searching for yaml test cases. + // It is by default directory to current git repository, or you can override + // the base directory with 'SQL_CASE_BASE_DIR' environment variable static std::string SqlCaseBaseDir(); static bool IsDebug() { diff --git a/hybridse/src/case/sql_case.cc b/hybridse/src/case/sql_case.cc index ccb712bbdf9..ac8e2459bd5 100644 --- a/hybridse/src/case/sql_case.cc +++ b/hybridse/src/case/sql_case.cc @@ -35,6 +35,7 @@ #include "glog/logging.h" #include "node/sql_node.h" #include "plan/plan_api.h" +#include "case/test_cfg.h" #include "vm/engine.h" #include "zetasql/parser/parser.h" #include "planv2/ast_node_converter.h" @@ -1762,11 +1763,7 @@ std::string SqlCase::SqlCaseBaseDir() { if (value != nullptr) { return std::string(value); } - value = getenv("YAML_CASE_BASE_DIR"); - if (value != nullptr) { - return std::string(value); - } - return ""; + return SQL_CASE_BASE_DIR; } absl::StatusOr> ExtractInsertRow(vm::HybridSeJitWrapper* jit, absl::string_view insert, diff --git a/hybridse/src/case/test_cfg.h.in b/hybridse/src/case/test_cfg.h.in new file mode 100644 index 00000000000..76709fa9af8 --- /dev/null +++ b/hybridse/src/case/test_cfg.h.in @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2024 OpenMLDB Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef HYBRIDSE_SRC_CASE_TEST_CFG_H_ +#define HYBRIDSE_SRC_CASE_TEST_CFG_H_ + +#define SQL_CASE_BASE_DIR "${CMAKE_SOURCE_DIR}" + +#endif // HYBRIDSE_SRC_CASE_TEST_CFG_H_ diff --git a/hybridse/src/vm/jit.cc b/hybridse/src/vm/jit.cc index 2bcf0d7ab39..243c0d8f4e8 100644 --- a/hybridse/src/vm/jit.cc +++ b/hybridse/src/vm/jit.cc @@ -29,6 +29,7 @@ extern "C" { #include "llvm/ExecutionEngine/JITSymbol.h" #include "llvm/ExecutionEngine/Orc/CompileUtils.h" #include "llvm/ExecutionEngine/Orc/Core.h" +#include "llvm/ExecutionEngine/JITEventListener.h" #include "llvm/ExecutionEngine/Orc/ExecutionUtils.h" #include "llvm/ExecutionEngine/Orc/IRCompileLayer.h" #include "llvm/ExecutionEngine/Orc/IRTransformLayer.h" @@ -314,7 +315,7 @@ bool HybridSeMcJitWrapper::AddModule( } else { execution_engine_->addModule(std::move(module)); } - if (jit_options_.IsEnableVTune()) { + if (jit_options_.IsEnableVtune()) { auto listener = ::llvm::JITEventListener::createIntelJITEventListener(); if (listener == nullptr) { LOG(WARNING) << "Intel jit events is not enabled"; @@ -322,7 +323,7 @@ bool HybridSeMcJitWrapper::AddModule( execution_engine_->RegisterJITEventListener(listener); } } - if (jit_options_.IsEnableGDB()) { + if (jit_options_.IsEnableGdb()) { auto listener = ::llvm::JITEventListener::createGDBRegistrationListener(); if (listener == nullptr) { From 3a7228a536f17ccae0ce82dc69b3391cc280a0d7 Mon Sep 17 00:00:00 2001 From: Siqi Wang Date: Thu, 25 Jul 2024 18:42:26 +0800 Subject: [PATCH 39/41] docs: add blog post (#3913) * Include new posts * update links * minor change --- docs/en/blog_post/20240503_OpenmldbRelease.md | 56 +++++++++++++++++++ docs/en/blog_post/index.rst | 5 +- docs/zh/blog_post/20240503_OpenmldbRelease.md | 43 ++++++++++++++ docs/zh/blog_post/index.rst | 5 +- 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 docs/en/blog_post/20240503_OpenmldbRelease.md create mode 100644 docs/zh/blog_post/20240503_OpenmldbRelease.md diff --git a/docs/en/blog_post/20240503_OpenmldbRelease.md b/docs/en/blog_post/20240503_OpenmldbRelease.md new file mode 100644 index 00000000000..adac57fa0c2 --- /dev/null +++ b/docs/en/blog_post/20240503_OpenmldbRelease.md @@ -0,0 +1,56 @@ +# OpenMLDB v0.9.0 Release: Major Upgrade in SQL Capabilities Covering the Entire Feature Servicing Process + +OpenMLDB has just released a new version v0.9.0, including SQL syntax extensions, MySQL protocol compatibility, TiDB storage support, online feature computation, feature signatures, and more. Among these, the most noteworthy features are the MySQL protocol and ANSI SQL compatibility, along with the extended SQL syntax capabilities. + +Firstly, MySQL protocol compatibility allows OpenMLDB users to access OpenMLDB clusters using any MySQL client, not limited to GUI applications like NaviCat or Sequal Ace but also Java JDBC MySQL Driver, Python SQLAlchemy, Go MySQL Driver, and various programming language SDKs. For more information, you can refer to "[**Ultra High-Performance Database OpenM(ysq)LDB: Seamless Compatibility with MySQL Protocol and Multi-Language MySQL Client**](20240322_Openmysqldb.md)". + +Secondly, the new version significantly expands SQL capabilities, especially implementing OpenMLDB’s unique request mode and stored procedure execution within standard SQL syntax. Compared to traditional SQL databases, OpenMLDB covers the entire machine learning process, including offline and online modes. In online mode, users can input sample data, and get feature results through SQL feature extraction. On the contrary, in the past, we needed to deploy SQL as a stored procedure through the `Deploy` command and then perform online feature computation through SDKs or HTTP interfaces. The new version adds `SELECT CONFIG` and `CALL` statements, allowing users to directly specify request mode and sample data in SQL to compute feature results, as shown below: + +``` +-- Execute online request mode query for action (10, "foo", timestamp(4000)) +SELECT id, count(val) over (partition by id order by ts rows between 10 preceding and current row) +FROM t1 +CONFIG (execute_mode = 'online', values = (10, "foo", timestamp(4000))) +``` +You can also use the ANSI SQL `CALL` statement to invoke stored procedures with sample rows as parameters, as shown below: + +``` +-- Execute online request mode query for action (10, "foo", timestamp(4000)) +DEPLOY window_features SELECT id, count(val) over (partition by id order by ts rows between 10 preceding and current row) +FROM t1; + +CALL window_features(10, "foo", timestamp(4000)) +``` +For detailed release notes, please refer to: [https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0](https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0) + +Please feel free to download and explore the latest release. Your feedback is highly valued and appreciated. We encourage you to share your thoughts and suggestions to help us improve and enhance the platform. Thank you for your support! + +## Release Date + +April 25, 2024 + +## Release Note + +[https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0](https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0) + +## Highlighted Features + +* Added support for the latest version of SQLAlchemy 2, seamlessly integrating with popular Python frameworks such as Pandas and Numpy. + +* Expanded support for more data backends, integrating TiDB’s distributed file storage capability with OpenMLDB’s high-performance in-memory feature computation capability. + +* Enhanced ANSI SQL support, fixed `first_value` semantics, supported `MAP` type and feature signatures, and added offline mode support for `INSERT` statements. + +* Added support for MySQL protocol, allowing access to OpenMLDB clusters using MySQL clients like NaviCat, Sequal Ace, and various MySQL SDKs for programming languages. + +* Extended SQL syntax support, enabling online feature computation directly through `SELECT CONFIG` or `CALL` statements. + +-------------------------------------------------------------------------------------------------------------- + +**For more information on OpenMLDB:** +* Official website: [https://openmldb.ai/](https://openmldb.ai/) +* GitHub: [https://github.com/4paradigm/OpenMLDB](https://github.com/4paradigm/OpenMLDB) +* Documentation: [https://openmldb.ai/docs/en/](https://openmldb.ai/docs/en/) +* Join us on [**Slack**](https://join.slack.com/t/openmldb/shared_invite/zt-ozu3llie-K~hn9Ss1GZcFW2~K_L5sMg)! + +> _This post is a re-post from [OpenMLDB Blogs](https://openmldb.medium.com/)._ diff --git a/docs/en/blog_post/index.rst b/docs/en/blog_post/index.rst index 5651599ff1c..20757e3a3e2 100644 --- a/docs/en/blog_post/index.rst +++ b/docs/en/blog_post/index.rst @@ -13,4 +13,7 @@ OpenMLDB Blogs Comparative Analysis of Memory Consumption: OpenMLDB vs Redis Test Report <20240402_OpenmldbVsRedis.md> - Introducing OpenMLDB’s New Feature: Feature Signatures — Enabling Complete Feature Engineering with SQL <20240523_OpenmldbFeatureSignatures.md> \ No newline at end of file + OpenMLDB v0.9.0 Release: Major Upgrade in SQL Capabilities Covering the Entire Feature Servicing Process <20240503_OpenmldbRelease.md> + + Introducing OpenMLDB’s New Feature: Feature Signatures — Enabling Complete Feature Engineering with SQL <20240523_OpenmldbFeatureSignatures.md> + diff --git a/docs/zh/blog_post/20240503_OpenmldbRelease.md b/docs/zh/blog_post/20240503_OpenmldbRelease.md new file mode 100644 index 00000000000..60cb65d3e39 --- /dev/null +++ b/docs/zh/blog_post/20240503_OpenmldbRelease.md @@ -0,0 +1,43 @@ +# OpenMLDB v0.9.0 发布:SQL 能力大升级覆盖特征上线全流程 + +## 发布日期 + +25 April 2024 + +## Release note + +[https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0](https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0) + + +## 亮点特性 +- 增加最新版 SQLAlchemy 2 的支持,无缝集成 Pandas 和 Numpy 等常用 Python 框架。 +- 支持更多数据后端,融合 TiDB 的分布式文件存储能力以及 OpenMLDB 内存高性能特征计算能力。 +- 完善 ANSI SQL 支持,修复 `first_value` 语义,支持 MAP 类型和特征签名,离线模式支持 `INSERT` 语句。 +- 支持 MySQL 协议,可用 NaviCat、Sequal Ace 及各种编程语言的 MySQL SDK 访问 OpenMLDB 集群。 +- 支持 SQL 语法拓展,通过 `SELECT CONFIG` 或 `CALL` 语句直接进行在线特征计算。 + + +社区朋友们大家好!OpenMLDB 正常发布了一个新的版本 v0.9.0,包含了 SQL 语法拓展、MySQL 协议兼容、TiDB 存储支持、在线执行特征计算、特征签名等功能,其中最值得关注和分享的就是对 MySQL 协议和 ANSI SQL 兼容的特性,以及本地拓展的 SQL 语法能力。 +首先 MySQL 协议兼容让 OpenMLDB 的用户,可以使用任意的 MySQL 客户端来访问 OpenMLDB 集群,不仅限于 NaviCat、Sequal Ace 等 GUI 应用,还可以使用 Java JDBC MySQL Driver、Python SQLAlchemy、Go MySQL Driver 等各种编程语言的 SDK。更多介绍可以参考 《[超高性能数据库 OpenM(ysq)LDB:无缝兼容 MySQL 协议 和多语言 MySQL 客户端](20240322_Openmysqldb.md)》 。 + +其次新版本极大拓展了 SQL 的能力,尤其是在标准 SQL 语法上实现了 OpenMLDB 特有的请求模式和存储过程的执行。相比于传统的 SQL 数据库,OpenMLDB 覆盖机器学习的全流程,包含离线模式和在线模式,在线模式下支持用户传入单行样本数据,通过 SQL 特征抽取返回特征结果。过去我们需要先通过 `Deploy` 命令部署 SQL 成存储过程,然后通过 SDK 或 HTTP 接口进行在线特征计算。新版本加入了 `SELECT CONFIG` 和 `CALL` 语句,用户在 SQL 中直接指定请求模式和请求样本就可以计算得到特征结果,示例如下。 + +``` +-- 执行请求行为 (10, "foo", timestamp(4000)) 的在线请求模式 query +SELECT id, count (val) over (partition by id order by ts rows between 10 preceding and current row) +FROM t1 +CONFIG (execute_mode = 'online', values = (10, "foo", timestamp (4000))) +``` + +也可以通过 ANSI SQL 的 `CALL`语句,以样本行作为参数传入进行存储过程的调用,示例如下。 + +``` +-- 执行请求行为 (10, "foo", timestamp(4000)) 的在线请求模式 query +DEPLOY window_features SELECT id, count (val) over (partition by id order by ts rows between 10 preceding and current row) +FROM t1; + +CALL window_features(10, "foo", timestamp(4000)) +``` + +详细的 release note 参照: [https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0](https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0) +欢迎大家下载试用,提供意见。 \ No newline at end of file diff --git a/docs/zh/blog_post/index.rst b/docs/zh/blog_post/index.rst index 89a445909d7..baabc6aa7e0 100644 --- a/docs/zh/blog_post/index.rst +++ b/docs/zh/blog_post/index.rst @@ -14,4 +14,7 @@ OpenMLDB vs Redis 内存占用量测试报告 <20240402_OpenmldbVsRedis.md> - OpenMLDB 新功能介绍:特征签名,让 SQL 完成特征工程全流程 <20240523_OpenmldbFeatureSignatures.md> \ No newline at end of file + OpenMLDB v0.9.0 发布:SQL 能力大升级覆盖特征上线全流程 <20240503_OpenmldbRelease.md> + + OpenMLDB 新功能介绍:特征签名,让 SQL 完成特征工程全流程 <20240523_OpenmldbFeatureSignatures.md> + From 1e07589053941e077ef55fea94fea7c3daf14f60 Mon Sep 17 00:00:00 2001 From: Jayaprakash0511 <123923815+Jayaprakash0511@users.noreply.github.com> Date: Fri, 26 Jul 2024 07:53:47 +0530 Subject: [PATCH 40/41] ci: update create-pull-request action to v6 in udf-doc-gen workflow & rm deprecated file sync (#3964) * Updated create-pull-request action to v6 in udf-doc-gen workflow * Removed references to docs/en/reference/sql/udfs_8h.md as the file no longer exists --- .github/workflows/udf-doc.yml | 3 +-- hybridse/tools/documentation/udf_doxygen/Makefile | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/udf-doc.yml b/.github/workflows/udf-doc.yml index b18263fcbc7..492ea6ebd47 100644 --- a/.github/workflows/udf-doc.yml +++ b/.github/workflows/udf-doc.yml @@ -50,11 +50,10 @@ jobs: make -C hybridse/tools/documentation/udf_doxygen sync - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v6 if: github.event_name != 'pull_request' with: add-paths: | - docs/en/reference/sql/udfs_8h.md docs/zh/openmldb_sql/udfs_8h.md labels: | udf diff --git a/hybridse/tools/documentation/udf_doxygen/Makefile b/hybridse/tools/documentation/udf_doxygen/Makefile index d3e8a344ba2..ecd3dd9462d 100644 --- a/hybridse/tools/documentation/udf_doxygen/Makefile +++ b/hybridse/tools/documentation/udf_doxygen/Makefile @@ -27,7 +27,6 @@ doxygen2md: doxygen sync: doxygen2md @if [ -n "$(SYNC_DIR)" ]; then \ - cp -v "$(UDF_GEN_DIR)/Files/udfs_8h.md" "$(SYNC_DIR)/docs/en/reference/sql/udfs_8h.md"; \ cp -v "$(UDF_GEN_DIR)/Files/udfs_8h.md" "$(SYNC_DIR)/docs/zh/openmldb_sql/udfs_8h.md"; \ else \ echo "SKIP SYNC: DEFAULT Sync DIR not found"; \ From b27826121b4b19df77f388ab61a6ac59b4de5a71 Mon Sep 17 00:00:00 2001 From: aceforeverd Date: Fri, 26 Jul 2024 10:42:29 +0800 Subject: [PATCH 41/41] build: upgrade openmldb sdk version in self host (#3962) --- .../openmldb-test-java/openmldb-sdk-test/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration-test/openmldb-test-java/openmldb-sdk-test/pom.xml b/test/integration-test/openmldb-test-java/openmldb-sdk-test/pom.xml index 32c81920daa..28c15820eda 100644 --- a/test/integration-test/openmldb-test-java/openmldb-sdk-test/pom.xml +++ b/test/integration-test/openmldb-test-java/openmldb-sdk-test/pom.xml @@ -15,8 +15,8 @@ 8 8 UTF-8 - 0.7.0-SNAPSHOT - 0.7.0-SNAPSHOT + 0.9.0 + 0.9.0 2.2.0 test_suite/test_tmp.xml 1.8.9 @@ -212,4 +212,4 @@ - \ No newline at end of file +