From b0a62d7274b588bfd77f29afd74bbca343155bfd Mon Sep 17 00:00:00 2001 From: ybbh Date: Sun, 12 Apr 2026 23:41:15 +0800 Subject: [PATCH] partition --- Cargo.lock | 475 +----- Cargo.toml | 2 + doc/cn/how_to_start.cn.md | 13 +- doc/cn/partition.cn.md | 275 ++++ doc/cn/session.cn.md | 27 +- doc/en/how_to_start.md | 13 +- doc/en/partition.md | 270 +++ doc/en/session.md | 27 +- doc/en/syscall.md | 9 +- doc/lang.common/mudu_batch.md | 5 +- example/key-value/Cargo.toml | 11 +- example/key-value/src/rust/procedure.rs | 61 + example/tpcc/Cargo.toml | 49 + example/tpcc/Makefile.toml | 66 + example/tpcc/README.md | 114 ++ example/tpcc/build-cfg/transpiler-cfg.toml | 17 + example/tpcc/package/package.cfg.json | 6 + example/tpcc/package/package.desc.json | 316 ++++ example/tpcc/package/type.desc.json | 503 ++++++ example/tpcc/sql/ddl.sql | 93 ++ example/tpcc/sql/init.sql | 1 + example/tpcc/src/bin/tpcc_benchmark.rs | 600 +++++++ example/tpcc/src/generated/customer.rs | 1236 ++++++++++++++ example/tpcc/src/generated/district.rs | 781 +++++++++ example/tpcc/src/generated/history.rs | 872 ++++++++++ example/tpcc/src/generated/item.rs | 417 +++++ example/tpcc/src/generated/mod.rs | 11 + example/tpcc/src/generated/new_order.rs | 417 +++++ example/tpcc/src/generated/order_line.rs | 963 +++++++++++ example/tpcc/src/generated/orders.rs | 963 +++++++++++ example/tpcc/src/generated/procedure.rs | 1444 +++++++++++++++++ .../tpcc/src/generated/procedure_common.rs | 85 + example/tpcc/src/generated/stock.rs | 690 ++++++++ example/tpcc/src/generated/warehouse.rs | 508 ++++++ example/tpcc/src/lib.rs | 13 + example/tpcc/src/rust/customer.rs | 1236 ++++++++++++++ example/tpcc/src/rust/district.rs | 781 +++++++++ example/tpcc/src/rust/history.rs | 872 ++++++++++ example/tpcc/src/rust/item.rs | 417 +++++ example/tpcc/src/rust/mod.rs | 11 + example/tpcc/src/rust/new_order.rs | 417 +++++ example/tpcc/src/rust/order_line.rs | 963 +++++++++++ example/tpcc/src/rust/orders.rs | 963 +++++++++++ example/tpcc/src/rust/procedure.rs | 495 ++++++ example/tpcc/src/rust/procedure_common.rs | 85 + example/tpcc/src/rust/stock.rs | 690 ++++++++ example/tpcc/src/rust/warehouse.rs | 508 ++++++ example/vote/Cargo.toml | 10 +- example/vote/src/rust/options.rs | 36 + example/vote/src/rust/procedure.rs | 65 + example/vote/src/rust/users.rs | 31 + example/vote/src/rust/vote_actions.rs | 31 + example/vote/src/rust/vote_history_item.rs | 40 + example/vote/src/rust/vote_result.rs | 38 + example/vote/src/rust/votes.rs | 37 + example/wallet/package/package.desc.json | 82 +- example/wallet/package/type.desc.json | 84 +- example/wallet/src/generated/mod.rs | 2 +- example/wallet/src/generated/orders.rs | 931 ++++++----- example/wallet/src/generated/procedures.rs | 1304 +++++++++------ example/wallet/src/generated/transactions.rs | 1076 ++++++------ example/wallet/src/generated/users.rs | 1221 +++++++------- example/wallet/src/generated/wallets.rs | 639 ++++---- example/wallet/src/generated/warehouse.rs | 44 +- example/wallet/src/rust/orders.rs | 929 ++++++----- example/wallet/src/rust/procedures.rs | 57 +- example/wallet/src/rust/transactions.rs | 1074 ++++++------ example/wallet/src/rust/users.rs | 1219 +++++++------- example/wallet/src/rust/wallets.rs | 637 ++++---- example/wallet/src/rust/warehouse.rs | 42 +- example/ycsb/Cargo.toml | 5 + example/ycsb/README.md | 2 +- example/ycsb/src/bin/ycsb_benchmark.rs | 25 +- example/ycsb/src/lib.rs | 6 + example/ycsb/src/rust/procedure.rs | 46 + example/ycsb/src/rust/procedure_async.rs | 57 + mudu/src/common/len_payload.rs | 15 + mudu/src/common/slice.rs | 48 + mudu/src/common/update_delta.rs | 2 +- mudu/src/utils/buf.rs | 2 +- mudu/src/utils/json.rs | 52 + mudu/src/utils/msg_pack.rs | 33 +- mudu/src/utils/toml.rs | 44 + mudu_adapter/src/backend.rs | 14 +- mudu_adapter/src/config.rs | 7 + mudu_adapter/src/mududb.rs | 137 +- mudu_adapter/src/mysql.rs | 35 + mudu_adapter/src/postgres.rs | 35 + mudu_adapter/tests/adapter_coverage_test.rs | 197 +++ mudu_api/rust/src/universal/test_uni.rs | 287 +++- mudu_api/rust/src/universal/uni_result.rs | 2 +- mudu_binding/src/universal/test_uni.rs | 287 +++- mudu_binding/src/universal/uni_result.rs | 2 +- mudu_cli/Cargo.toml | 1 + mudu_cli/README.md | 16 +- mudu_cli/src/binding_api.rs | 113 +- mudu_cli/src/client/async_client.rs | 25 +- mudu_cli/src/client/client.rs | 8 +- mudu_cli/src/client/json_client.rs | 61 +- mudu_cli/src/main.rs | 42 +- mudu_cli/src/management.rs | 91 +- mudu_contract/src/database/err_no.rs | 28 - mudu_contract/src/database/filter.rs | 48 - mudu_contract/src/database/mod.rs | 5 - mudu_contract/src/database/predicate.rs | 19 - mudu_contract/src/database/project.rs | 21 - mudu_contract/src/database/table.rs | 45 - mudu_contract/src/protocol.rs | 36 +- mudu_contract/src/tuple/build_tuple.rs | 22 +- mudu_contract/src/tuple/read_datum.rs | 2 +- mudu_contract/src/tuple/tuple_binary_desc.rs | 7 +- mudu_contract/src/tuple/tuple_value.rs | 2 + mudu_contract/src/tuple/write_value.rs | 21 +- .../src/command/create_partition_placement.rs | 44 + .../src/command/create_partition_rule.rs | 66 + mudu_kernel/src/command/create_table.rs | 6 +- mudu_kernel/src/command/mod.rs | 2 + mudu_kernel/src/contract/mem_store.rs | 20 - mudu_kernel/src/contract/meta_mgr.rs | 54 + mudu_kernel/src/contract/mod.rs | 7 +- mudu_kernel/src/contract/partition_rule.rs | 64 + .../src/contract/partition_rule_binding.rs | 15 + mudu_kernel/src/contract/pst_op.rs | 40 - mudu_kernel/src/contract/pst_op_list.rs | 60 - mudu_kernel/src/io/socket.rs | 2 +- mudu_kernel/src/lib.rs | 2 +- mudu_kernel/src/meta/meta_mgr.rs | 243 ++- mudu_kernel/src/meta/mod.rs | 3 + .../src/meta/partition_binding_catalog.rs | 133 ++ .../src/meta/partition_placement_catalog.rs | 133 ++ .../src/meta/partition_rule_catalog.rs | 133 ++ mudu_kernel/src/mudu_conn/mod.rs | 2 +- mudu_kernel/src/mudu_conn/mudu_conn_async.rs | 366 ++++- mudu_kernel/src/server/connection_state.rs | 12 + .../src/server/connection_worker_task.rs | 2 +- mudu_kernel/src/server/fsm.rs | 46 - mudu_kernel/src/server/message_bus_api.rs | 283 ++++ mudu_kernel/src/server/message_bus_runtime.rs | 355 ++++ mudu_kernel/src/server/mod.rs | 7 +- mudu_kernel/src/server/partition_router.rs | 379 +++++ mudu_kernel/src/server/partition_rpc.rs | 53 + mudu_kernel/src/server/request_ctx.rs | 28 +- mudu_kernel/src/server/routing.rs | 2 +- mudu_kernel/src/server/server.rs | 14 +- mudu_kernel/src/server/server_iouring.rs | 4 +- .../server/session_bound_worker_runtime.rs | 25 +- mudu_kernel/src/server/worker.rs | 60 +- mudu_kernel/src/server/worker_local.rs | 27 +- mudu_kernel/src/server/worker_mailbox.rs | 2 + mudu_kernel/src/server/worker_registry.rs | 4 + mudu_kernel/src/server/worker_ring_loop.rs | 35 +- mudu_kernel/src/server/worker_storage.rs | 314 +++- mudu_kernel/src/server/worker_tx_manager.rs | 49 +- mudu_kernel/src/server/x_contract.rs | 673 +++++++- mudu_kernel/src/server/x_lock_mgr.rs | 15 +- mudu_kernel/src/sql/binder.rs | 142 +- mudu_kernel/src/sql/bound_stmt.rs | 15 + mudu_kernel/src/sql/build_select.rs | 40 - mudu_kernel/src/sql/build_where_predicate.rs | 45 - mudu_kernel/src/sql/cmp_pred.rs | 14 - mudu_kernel/src/sql/describer.rs | 12 +- mudu_kernel/src/sql/mod.rs | 5 - mudu_kernel/src/sql/plan_param.rs | 18 - mudu_kernel/src/sql/planner.rs | 41 +- mudu_kernel/src/storage/mem_store.rs | 107 -- mudu_kernel/src/storage/mem_store_factory.rs | 12 - mudu_kernel/src/storage/mem_table.rs | 86 - mudu_kernel/src/storage/mod.rs | 10 - mudu_kernel/src/storage/pst_op_ch.rs | 6 - mudu_kernel/src/storage/pst_op_ch_impl.rs | 10 - mudu_kernel/src/storage/pst_store_factory.rs | 28 - mudu_kernel/src/storage/pst_store_impl.rs | 237 --- mudu_kernel/src/storage/relation/relation.rs | 25 +- mudu_kernel/src/storage/test_pst_store.rs | 68 - mudu_kernel/src/wal/worker_wal_backend.rs | 3 + mudu_kernel/src/wal/xl_batch_worker_log.rs | 1 + mudu_kernel/src/wal/xl_data_op.rs | 10 + mudu_kernel/src/x_engine/mod.rs | 2 +- mudu_kernel/src/x_engine/tx_mgr.rs | 21 +- mudu_kernel/src/x_engine/x_param.rs | 15 + mudu_runtime/Cargo.toml | 4 - mudu_runtime/src/backend/backend.rs | 2 +- .../src/backend/http_api/io_uring_http_api.rs | 100 +- .../src/backend/http_api/legacy_http_api.rs | 4 +- mudu_runtime/src/backend/http_api/mod.rs | 301 +++- mudu_runtime/src/backend/mod.rs | 4 +- mudu_runtime/src/backend/mudu_app_mgr.rs | 8 +- .../src/backend/sql_async_client_test.rs | 177 +- mudu_runtime/src/backend/test_backend.rs | 41 - mudu_runtime/src/backend/test_pg_cli.rs | 104 -- mudu_runtime/src/backend/test_sql.rs | 82 - mudu_runtime/src/backend/web_serve.rs | 1 + mudu_runtime/src/db_connector.rs | 25 +- .../src/db_libsql_async/libsql_async_conn.rs | 6 +- .../src/db_libsql_async/result_set.rs | 4 +- mudu_runtime/src/db_postgres/ddl_item.sql | 10 - mudu_runtime/src/db_postgres/mod.rs | 4 - .../src/db_postgres/pg_interactive_conn.rs | 208 --- mudu_runtime/src/db_postgres/result_set_pg.rs | 75 - mudu_runtime/src/db_postgres/test_conn.rs | 2 - mudu_runtime/src/db_postgres/tx_pg.rs | 58 - mudu_runtime/src/db_turso/mod.rs | 7 - mudu_runtime/src/db_turso/param.rs | 21 - mudu_runtime/src/db_turso/result_set.rs | 169 -- mudu_runtime/src/db_turso/turso_conn.rs | 95 -- mudu_runtime/src/db_turso/turso_conn_inner.rs | 379 ----- mudu_runtime/src/db_turso/turso_desc.rs | 43 - mudu_runtime/src/lib.rs | 2 - mudu_runtime/src/resolver/filter.rs | 24 - mudu_runtime/src/resolver/item_value.rs | 6 - mudu_runtime/src/resolver/mod.rs | 8 - mudu_runtime/src/resolver/resolved_command.rs | 5 - mudu_runtime/src/resolver/resolved_insert.rs | 33 - mudu_runtime/src/resolver/resolved_select.rs | 49 - mudu_runtime/src/resolver/resolved_type.rs | 5 - mudu_runtime/src/resolver/resolved_update.rs | 52 - mudu_runtime/src/resolver/schema_mgr.rs | 65 +- mudu_runtime/src/resolver/sql_resolver.rs | 253 --- mudu_runtime/src/service/app_inst_impl.rs | 52 +- mudu_runtime/src/service/runtime_opt.rs | 3 + mudu_runtime/src/service/runtime_simple.rs | 1 + .../src/service/runtime_simple_test.rs | 1 + mudu_runtime/src/service/service_impl.rs | 4 +- .../src/service/wt_runtime_component_test.rs | 1 + mudu_sys/Cargo.toml | 4 +- mudu_sys/src/env.rs | 17 +- mudu_sys/src/fd.rs | 2 +- mudu_sys/src/lib.rs | 3 + mudu_sys/src/portable/env.rs | 252 +++ mudu_sys/src/portable/mod.rs | 1 + mudu_sys/src/task.rs | 2 + mudu_type/src/dat_value.rs | 59 +- mudu_type/src/dt_impl/compare_test.rs | 109 ++ mudu_type/src/dt_impl/error_test.rs | 188 +++ mudu_type/src/dt_impl/fn_array_arb.rs | 22 +- mudu_type/src/dt_impl/fn_binary.rs | 22 +- mudu_type/src/dt_impl/fn_f32.rs | 6 +- mudu_type/src/dt_impl/fn_f32_arb.rs | 9 +- mudu_type/src/dt_impl/fn_f64.rs | 2 +- mudu_type/src/dt_impl/fn_f64_arb.rs | 9 +- mudu_type/src/dt_impl/fn_i128_arb.rs | 2 +- mudu_type/src/dt_impl/fn_i32.rs | 2 +- mudu_type/src/dt_impl/fn_i64.rs | 4 +- mudu_type/src/dt_impl/fn_object_arb.rs | 74 +- mudu_type/src/dt_impl/fn_string_arb.rs | 2 +- mudu_type/src/dt_impl/fn_u128_arb.rs | 2 +- mudu_type/src/dt_impl/generic_prop_test.rs | 244 +++ mudu_type/src/dt_impl/mod.rs | 15 + mudu_type/src/dt_impl/object_array_test.rs | 141 ++ mudu_type/src/dt_impl/param_test.rs | 66 + mudu_utils/src/sync/async_task.rs | 61 +- mudu_utils/src/sync/notify_wait.rs | 20 + mudu_utils/src/sync/s_task.rs | 40 + sql_parser/src/ast/ast_node.rs | 5 - sql_parser/src/ast/expr_arithmetic.rs | 29 + sql_parser/src/ast/expr_compare.rs | 38 + sql_parser/src/ast/expr_item.rs | 36 + sql_parser/src/ast/expr_literal.rs | 13 + sql_parser/src/ast/expr_operator.rs | 3 +- sql_parser/src/ast/mod.rs | 3 + sql_parser/src/ast/parser.rs | 310 ++++ sql_parser/src/ast/parser_test.rs | 112 ++ sql_parser/src/ast/stmt_copy_to.rs | 18 + .../ast/stmt_create_partition_placement.rs | 49 + .../src/ast/stmt_create_partition_rule.rs | 57 + sql_parser/src/ast/stmt_create_table.rs | 11 + sql_parser/src/ast/stmt_table_partition.rs | 26 + sql_parser/src/ast/stmt_type.rs | 4 + sql_parser/src/ast/type_declare.rs | 18 + sys_interface/Cargo.toml | 5 + sys_interface/src/api_impl/mod.rs | 47 + sys_interface/src/host.rs | 95 ++ sys_interface/standalone_adapter.md | 8 +- sys_interface/tests/standalone_api_test.rs | 219 +++ testing/Cargo.toml | 19 + testing/Makefile.toml | 27 + testing/mpk/wallet.mpk | Bin 0 -> 1108807 bytes testing/src/lib.rs | 57 + testing/tests/wallet_mpk.rs | 563 +++++++ 279 files changed, 32480 insertions(+), 8300 deletions(-) create mode 100644 doc/cn/partition.cn.md create mode 100644 doc/en/partition.md create mode 100644 example/tpcc/Cargo.toml create mode 100644 example/tpcc/Makefile.toml create mode 100644 example/tpcc/README.md create mode 100644 example/tpcc/build-cfg/transpiler-cfg.toml create mode 100644 example/tpcc/package/package.cfg.json create mode 100644 example/tpcc/package/package.desc.json create mode 100644 example/tpcc/package/type.desc.json create mode 100644 example/tpcc/sql/ddl.sql create mode 100644 example/tpcc/sql/init.sql create mode 100644 example/tpcc/src/bin/tpcc_benchmark.rs create mode 100644 example/tpcc/src/generated/customer.rs create mode 100644 example/tpcc/src/generated/district.rs create mode 100644 example/tpcc/src/generated/history.rs create mode 100644 example/tpcc/src/generated/item.rs create mode 100644 example/tpcc/src/generated/mod.rs create mode 100644 example/tpcc/src/generated/new_order.rs create mode 100644 example/tpcc/src/generated/order_line.rs create mode 100644 example/tpcc/src/generated/orders.rs create mode 100644 example/tpcc/src/generated/procedure.rs create mode 100644 example/tpcc/src/generated/procedure_common.rs create mode 100644 example/tpcc/src/generated/stock.rs create mode 100644 example/tpcc/src/generated/warehouse.rs create mode 100644 example/tpcc/src/lib.rs create mode 100644 example/tpcc/src/rust/customer.rs create mode 100644 example/tpcc/src/rust/district.rs create mode 100644 example/tpcc/src/rust/history.rs create mode 100644 example/tpcc/src/rust/item.rs create mode 100644 example/tpcc/src/rust/mod.rs create mode 100644 example/tpcc/src/rust/new_order.rs create mode 100644 example/tpcc/src/rust/order_line.rs create mode 100644 example/tpcc/src/rust/orders.rs create mode 100644 example/tpcc/src/rust/procedure.rs create mode 100644 example/tpcc/src/rust/procedure_common.rs create mode 100644 example/tpcc/src/rust/stock.rs create mode 100644 example/tpcc/src/rust/warehouse.rs create mode 100644 mudu_adapter/tests/adapter_coverage_test.rs delete mode 100644 mudu_contract/src/database/err_no.rs delete mode 100644 mudu_contract/src/database/filter.rs delete mode 100644 mudu_contract/src/database/predicate.rs delete mode 100644 mudu_contract/src/database/project.rs delete mode 100644 mudu_contract/src/database/table.rs create mode 100644 mudu_kernel/src/command/create_partition_placement.rs create mode 100644 mudu_kernel/src/command/create_partition_rule.rs delete mode 100644 mudu_kernel/src/contract/mem_store.rs create mode 100644 mudu_kernel/src/contract/partition_rule.rs create mode 100644 mudu_kernel/src/contract/partition_rule_binding.rs delete mode 100644 mudu_kernel/src/contract/pst_op.rs delete mode 100644 mudu_kernel/src/contract/pst_op_list.rs create mode 100644 mudu_kernel/src/meta/partition_binding_catalog.rs create mode 100644 mudu_kernel/src/meta/partition_placement_catalog.rs create mode 100644 mudu_kernel/src/meta/partition_rule_catalog.rs create mode 100644 mudu_kernel/src/server/connection_state.rs delete mode 100644 mudu_kernel/src/server/fsm.rs create mode 100644 mudu_kernel/src/server/message_bus_api.rs create mode 100644 mudu_kernel/src/server/message_bus_runtime.rs create mode 100644 mudu_kernel/src/server/partition_router.rs create mode 100644 mudu_kernel/src/server/partition_rpc.rs delete mode 100644 mudu_kernel/src/sql/build_select.rs delete mode 100644 mudu_kernel/src/sql/build_where_predicate.rs delete mode 100644 mudu_kernel/src/sql/cmp_pred.rs delete mode 100644 mudu_kernel/src/sql/plan_param.rs delete mode 100644 mudu_kernel/src/storage/mem_store.rs delete mode 100644 mudu_kernel/src/storage/mem_store_factory.rs delete mode 100644 mudu_kernel/src/storage/mem_table.rs delete mode 100644 mudu_kernel/src/storage/pst_op_ch.rs delete mode 100644 mudu_kernel/src/storage/pst_op_ch_impl.rs delete mode 100644 mudu_kernel/src/storage/pst_store_factory.rs delete mode 100644 mudu_kernel/src/storage/pst_store_impl.rs delete mode 100644 mudu_kernel/src/storage/test_pst_store.rs delete mode 100644 mudu_runtime/src/backend/test_pg_cli.rs delete mode 100644 mudu_runtime/src/backend/test_sql.rs delete mode 100644 mudu_runtime/src/db_postgres/ddl_item.sql delete mode 100644 mudu_runtime/src/db_postgres/mod.rs delete mode 100644 mudu_runtime/src/db_postgres/pg_interactive_conn.rs delete mode 100644 mudu_runtime/src/db_postgres/result_set_pg.rs delete mode 100644 mudu_runtime/src/db_postgres/test_conn.rs delete mode 100644 mudu_runtime/src/db_postgres/tx_pg.rs delete mode 100644 mudu_runtime/src/db_turso/mod.rs delete mode 100644 mudu_runtime/src/db_turso/param.rs delete mode 100644 mudu_runtime/src/db_turso/result_set.rs delete mode 100644 mudu_runtime/src/db_turso/turso_conn.rs delete mode 100644 mudu_runtime/src/db_turso/turso_conn_inner.rs delete mode 100644 mudu_runtime/src/db_turso/turso_desc.rs delete mode 100644 mudu_runtime/src/resolver/filter.rs delete mode 100644 mudu_runtime/src/resolver/item_value.rs delete mode 100644 mudu_runtime/src/resolver/resolved_command.rs delete mode 100644 mudu_runtime/src/resolver/resolved_insert.rs delete mode 100644 mudu_runtime/src/resolver/resolved_select.rs delete mode 100644 mudu_runtime/src/resolver/resolved_type.rs delete mode 100644 mudu_runtime/src/resolver/resolved_update.rs delete mode 100644 mudu_runtime/src/resolver/sql_resolver.rs create mode 100644 mudu_sys/src/portable/env.rs create mode 100644 mudu_sys/src/portable/mod.rs create mode 100644 mudu_type/src/dt_impl/compare_test.rs create mode 100644 mudu_type/src/dt_impl/error_test.rs create mode 100644 mudu_type/src/dt_impl/generic_prop_test.rs create mode 100644 mudu_type/src/dt_impl/object_array_test.rs create mode 100644 mudu_type/src/dt_impl/param_test.rs create mode 100644 sql_parser/src/ast/stmt_create_partition_placement.rs create mode 100644 sql_parser/src/ast/stmt_create_partition_rule.rs create mode 100644 sql_parser/src/ast/stmt_table_partition.rs create mode 100644 sys_interface/tests/standalone_api_test.rs create mode 100644 testing/Cargo.toml create mode 100644 testing/Makefile.toml create mode 100644 testing/mpk/wallet.mpk create mode 100644 testing/src/lib.rs create mode 100644 testing/tests/wallet_mpk.rs diff --git a/Cargo.lock b/Cargo.lock index 602efe4..cb5b129 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,26 +215,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.7", - "generic-array", -] - -[[package]] -name = "aegis" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f316fbfbb9de4b7981010671fc9a999ed6017b9c623ca3cc21c7af0833a1ca" -dependencies = [ - "cc", - "softaes", -] - [[package]] name = "aes" version = "0.8.4" @@ -246,20 +226,6 @@ dependencies = [ "cpufeatures 0.2.17", ] -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - [[package]] name = "ahash" version = "0.7.8" @@ -403,15 +369,6 @@ dependencies = [ "derive_arbitrary", ] -[[package]] -name = "arc-swap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" -dependencies = [ - "rustversion", -] - [[package]] name = "array-init" version = "2.1.0" @@ -931,12 +888,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "bit-vec" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b4ff8b16e6076c3e14220b39fbc1fabb6737522281a388998046859400895f" - [[package]] name = "bitflags" version = "1.3.2" @@ -1010,15 +961,6 @@ dependencies = [ "piper", ] -[[package]] -name = "bloom" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00ac8e5056d6d65376a3c1aa5c7c34850d6949ace17f0266953a254eb3d6fe8" -dependencies = [ - "bit-vec", -] - [[package]] name = "borsh" version = "1.6.0" @@ -1087,16 +1029,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" -[[package]] -name = "built" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" -dependencies = [ - "chrono", - "git2", -] - [[package]] name = "bumpalo" version = "3.19.1" @@ -1128,26 +1060,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "byteorder" version = "1.5.0" @@ -1363,12 +1275,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "cfg_block" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18758054972164c3264f7c8386f5fc6da6114cb46b619fd365d4e3b2dc3ae487" - [[package]] name = "chacha20" version = "0.10.0" @@ -1830,16 +1736,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-skiplist" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1853,7 +1749,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -1889,15 +1784,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - [[package]] name = "darling" version = "0.20.11" @@ -2481,16 +2367,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - [[package]] name = "gimli" version = "0.33.0" @@ -2503,19 +2379,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "git2" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" -dependencies = [ - "bitflags 2.10.0", - "libc", - "libgit2-sys", - "log", - "url", -] - [[package]] name = "glob" version = "0.3.3" @@ -3100,15 +2963,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "intrusive-collections" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" -dependencies = [ - "memoffset", -] - [[package]] name = "io-enum" version = "1.2.1" @@ -3134,17 +2988,6 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" -[[package]] -name = "io-uring" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -3289,6 +3132,7 @@ name = "key-value" version = "0.1.0" dependencies = [ "mudu", + "mudu_adapter", "mudu_binding", "mudu_contract", "mudu_type", @@ -3379,18 +3223,6 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" -[[package]] -name = "libgit2-sys" -version = "0.18.3+1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - [[package]] name = "libloading" version = "0.8.9" @@ -3407,16 +3239,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" -[[package]] -name = "libmimalloc-sys" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "libredox" version = "0.1.11" @@ -3591,7 +3413,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", - "libc", "pkg-config", "vcpkg", ] @@ -3769,46 +3590,6 @@ dependencies = [ "rustix 1.1.3", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "miette" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" -dependencies = [ - "cfg-if", - "miette-derive", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "mimalloc" -version = "0.1.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" -dependencies = [ - "libmimalloc-sys", -] - [[package]] name = "mime" version = "0.3.17" @@ -3930,6 +3711,7 @@ dependencies = [ "mudu", "mudu_binding", "mudu_contract", + "mudu_type", "reqwest", "serde", "serde_json", @@ -4075,19 +3857,17 @@ dependencies = [ "mudu_type", "mudu_utils", "pgwire", - "postgres", "scc", "scopeguard", "serde", "serde_json", "serde_repr", "sql_parser", - "strum 0.27.2", - "strum_macros 0.27.2", + "strum", + "strum_macros", "tokio", "toml 1.1.0+spec-1.1.0", "tracing", - "turso", "uuid", "wasmtime", "wasmtime-wasi", @@ -4413,12 +4193,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl-probe" version = "0.1.6" @@ -4431,15 +4205,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "pack1" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6e7cd9bd638dc2c831519a0caa1c006cab771a92b1303403a8322773c5b72d6" -dependencies = [ - "bytemuck", -] - [[package]] name = "parking" version = "2.2.1" @@ -4688,18 +4453,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "postcard" version = "1.1.3" @@ -5111,15 +4864,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rapidhash" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2988730ee014541157f48ce4dcc603940e00915edc3c7f9a8d78092256bb2493" -dependencies = [ - "rustversion", -] - [[package]] name = "rayon" version = "1.11.0" @@ -5349,16 +5093,6 @@ dependencies = [ "rmp", ] -[[package]] -name = "roaring" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" -dependencies = [ - "bytemuck", - "byteorder", -] - [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -5836,12 +5570,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - [[package]] name = "sha2" version = "0.10.9" @@ -5908,15 +5636,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "simsimd" -version = "6.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dcd49d13a0950ae06cd46597cfad99a9d23797e4e9f9e2b29decdc016b3f7" -dependencies = [ - "cc", -] - [[package]] name = "siphasher" version = "1.0.1" @@ -5984,12 +5703,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "softaes" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef461faaeb36c340b6c887167a9054a034f6acfc50a014ead26a02b4356b3de" - [[package]] name = "spki" version = "0.7.3" @@ -6065,34 +5778,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", -] - [[package]] name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.111", -] - [[package]] name = "strum_macros" version = "0.27.2" @@ -6176,6 +5867,7 @@ dependencies = [ "mudu_adapter", "mudu_binding", "mudu_contract", + "mudu_type", "thiserror 2.0.17", "tokio", "uniffi", @@ -6260,6 +5952,25 @@ dependencies = [ "arbitrary", ] +[[package]] +name = "testing" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "libsql", + "mudu", + "mudu_binding", + "mudu_cli", + "mudu_contract", + "mudu_runtime", + "mudu_sys", + "mudu_utils", + "reqwest", + "serde_json", + "tokio", + "tracing", +] + [[package]] name = "textwrap" version = "0.16.2" @@ -6736,6 +6447,28 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tpcc" +version = "0.1.0" +dependencies = [ + "async-backtrace", + "clap", + "lazy_static", + "mudu", + "mudu_adapter", + "mudu_binding", + "mudu_cli", + "mudu_contract", + "mudu_runtime", + "mudu_sys", + "mudu_type", + "mudu_utils", + "sys_interface", + "testing", + "tokio", + "wit-bindgen 0.54.0", +] + [[package]] name = "tracing" version = "0.1.44" @@ -6852,116 +6585,11 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "turso" -version = "0.4.0-pre.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f927be684ce9c612d2f11d04b9d48f4ba553fa98cf6e3bd2073c16602c8d7dc4" -dependencies = [ - "mimalloc", - "thiserror 2.0.17", - "tracing", - "tracing-subscriber", - "turso_core", -] - -[[package]] -name = "turso_core" -version = "0.4.0-pre.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88848a55b47fde014039f36dad09f33af34ad27ed94e70161fb8cf435ea3f3" -dependencies = [ - "aegis", - "aes", - "aes-gcm", - "arc-swap", - "bitflags 2.10.0", - "bloom", - "built", - "bytemuck", - "cfg_block", - "chrono", - "crossbeam-skiplist", - "either", - "fallible-iterator 0.3.0", - "hex", - "intrusive-collections", - "io-uring", - "libc", - "libloading", - "libm", - "miette", - "pack1", - "parking_lot", - "paste", - "polling", - "rand 0.9.2", - "rapidhash", - "regex", - "regex-syntax", - "roaring", - "rustc-hash 2.1.1", - "rustix 1.1.3", - "ryu", - "simsimd", - "strum 0.26.3", - "strum_macros 0.26.4", - "tempfile", - "thiserror 2.0.17", - "tracing", - "tracing-subscriber", - "turso_ext", - "turso_macros", - "turso_parser", - "twox-hash", - "uncased", - "uuid", -] - -[[package]] -name = "turso_ext" -version = "0.4.0-pre.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12e03badf97da72c41c3289ac19245109096e6262017151dad23ecf7115ee433" -dependencies = [ - "chrono", - "getrandom 0.3.4", - "turso_macros", -] - -[[package]] -name = "turso_macros" -version = "0.4.0-pre.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "681754e341a0420494203b73e067b0b69b29ccf0852d0d24bc7d5255d4441dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "turso_parser" -version = "0.4.0-pre.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92dd528809dfb32facb949b9b5416a33ee8f856b444f882854c0835428f511dd" -dependencies = [ - "bitflags 2.10.0", - "miette", - "strum 0.26.3", - "strum_macros 0.26.4", - "thiserror 2.0.17", - "turso_macros", -] - [[package]] name = "twox-hash" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" -dependencies = [ - "rand 0.9.2", -] [[package]] name = "typed-path" @@ -7155,16 +6783,6 @@ dependencies = [ "weedle2", ] -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.7", - "subtle", -] - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -7210,7 +6828,6 @@ dependencies = [ "getrandom 0.3.4", "js-sys", "serde_core", - "sha1_smol", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 120c0a9..11ac065 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ + "testing", "mudu", "mudu_sys", "mudu_adapter", @@ -13,6 +14,7 @@ members = [ "example/game-backend", "example/key-value", "example/ycsb", + "example/tpcc", "mudu_runtime", "mudu_gen", "sql_parser", diff --git a/doc/cn/how_to_start.cn.md b/doc/cn/how_to_start.cn.md index 134384d..db69f59 100644 --- a/doc/cn/how_to_start.cn.md +++ b/doc/cn/how_to_start.cn.md @@ -83,9 +83,12 @@ python script/build/install_binaries.py --all-workspace-bins 在以下位置创建配置文件: ```bash +mkdir -p ${HOME}/.mudu touch ${HOME}/.mudu/mududb_cfg.toml ``` +如果该文件不存在,`mudud` 首次启动时也会按默认值自动创建 `${HOME}/.mudu/mududb_cfg.toml`。 + ## 使用 MuduDB ### 1. 启动 `mudud` @@ -118,12 +121,12 @@ mudud 每条 `mcli` 命令都会自动创建并关闭一个临时 session,因此不需要显式传入 `session_id`。 ```bash -mcli put --json '{ +mcli --addr 127.0.0.1:9527 put --json '{ "key": "user-1", "value": "value-1" }' -mcli get --json '{ +mcli --addr 127.0.0.1:9527 get --json '{ "key": "user-1" }' ``` @@ -152,7 +155,7 @@ target/wasm32-wasip2/release/key-value.mpk #### 使用 mcli 安装 `.mpk` 包 ```bash -mcli app-install --mpk target/wasm32-wasip2/release/key-value.mpk +mcli --http-addr 127.0.0.1:8300 app-install --mpk target/wasm32-wasip2/release/key-value.mpk ``` #### 使用 mcli 调用已安装 `.mpk` 中的过程 @@ -160,7 +163,7 @@ mcli app-install --mpk target/wasm32-wasip2/release/key-value.mpk 通过 `kv_insert` 过程写入一条记录: ```bash -mcli app-invoke --app kv --module key_value --proc kv_insert --json '{ +mcli --addr 127.0.0.1:9527 --http-addr 127.0.0.1:8300 app-invoke --app kv --module key_value --proc kv_insert --json '{ "user_key": "user-1", "value": "value-from-mpk" }' @@ -169,7 +172,7 @@ mcli app-invoke --app kv --module key_value --proc kv_insert --json '{ 再通过 `kv_read` 过程读取: ```bash -mcli app-invoke --app kv --module key_value --proc kv_read --json '{ +mcli --addr 127.0.0.1:9527 --http-addr 127.0.0.1:8300 app-invoke --app kv --module key_value --proc kv_read --json '{ "user_key": "user-1" }' ``` diff --git a/doc/cn/partition.cn.md b/doc/cn/partition.cn.md new file mode 100644 index 0000000..e44879e --- /dev/null +++ b/doc/cn/partition.cn.md @@ -0,0 +1,275 @@ +# Partition 机制 + +本文介绍 MuduDB 当前的 partition 机制。 + +当前实现分为四层: + +- 全局 range 分区规则 +- 表到分区规则的绑定 +- partition 到 worker 的 placement +- 执行时的路由层 + +目标是让多张表复用同一套 range 切分规则,同时把不同 partition 分布到多个 worker 上。 + +## 核心概念 + +### Partition Rule + +partition rule 是一个全局元数据对象。 + +它定义了: + +- 分区方法,目前只支持 `RANGE` +- 一个或多个分区键列 +- 有序的分区边界 +- 一组逻辑 partition + +一个 rule 会展开成多个逻辑 partition。后续这些 partition 再通过 placement 元数据映射到具体 worker。 + +### Table Binding + +表本身不会把完整分区布局直接塞进 schema。 + +相反,表会绑定到一个已经存在的全局 rule,并声明本表哪些列引用这个 rule。 + +这样可以让多张表共享同一套 partition 布局。 + +在当前实现里,引用列应当与主键前缀一致。这样 point lookup、range pruning 和底层 key 编码模型可以保持一致。 + +### Placement + +placement 用来描述每个逻辑 partition 落到哪个 worker。 + +placement 被单独拆成一层元数据,原因是: + +- placement 属于部署问题,不属于 schema 本身 +- placement 的变化频率可能和表定义不同 +- 同一个 rule 可以被多张表复用 + +### Physical Relation + +在 worker 内部,relation storage 不再只按 `table_id` 建索引。 + +它使用下面这个物理标识: + +```text +(table_id, partition_id) +``` + +这是必须的,因为同一个 worker 上可能同时持有同一张逻辑表的多个 partition。 + +## 元数据模型 + +partition 元数据被拆成三类对象。 + +### `PartitionRuleDesc` + +用于定义全局 range rule: + +- `rule_id` +- `name` +- `kind` +- `key_types` +- `partitions` +- `version` + +每个 partition 定义里包含: + +- `partition_id` +- `name` +- `start` 边界,包含 +- `end` 边界,不包含 + +### `TablePartitionBinding` + +用于定义一张表如何引用一个全局 rule: + +- `table_id` +- `rule_id` +- `ref_attr_indices` + +其中 `ref_attr_indices` 表示表内哪些列组成分区键。 + +### `PartitionPlacement` + +用于描述 partition 的放置位置: + +- `partition_id` +- `worker_id` + +## Catalog + +partition 元数据持久化在单独的内部 catalog 中。 + +- `__meta_partition_rule` +- `__meta_table_partition_binding` +- `__meta_partition_placement` + +这些 catalog 由 `MetaMgr` 管理,并在 metadata 层缓存到内存中。 + +## DDL + +当前实现支持以下 DDL。 + +### 创建全局分区规则 + +```sql +CREATE PARTITION RULE r_orders +RANGE (region_id, order_id) ( + PARTITION p0 VALUES FROM (MINVALUE, MINVALUE) TO (1000, MINVALUE), + PARTITION p1 VALUES FROM (1000, MINVALUE) TO (2000, MINVALUE), + PARTITION p2 VALUES FROM (2000, MINVALUE) TO (MAXVALUE, MAXVALUE) +); +``` + +### 创建分区表 + +```sql +CREATE TABLE orders ( + region_id BIGINT, + order_id BIGINT, + amount BIGINT, + PRIMARY KEY(region_id, order_id) +) +PARTITION BY GLOBAL RULE r_orders +REFERENCES (region_id, order_id); +``` + +### 创建 partition placement + +```sql +CREATE PARTITION PLACEMENT FOR RULE r_orders ( + PARTITION p0 ON WORKER 1, + PARTITION p1 ON WORKER 2, + PARTITION p2 ON WORKER 3 +); +``` + +## 路由模型 + +路由由 `PartitionRouter` 实现。 + +对于分区表,router 会执行以下步骤: + +1. 加载表绑定信息。 +2. 加载该表引用的分区 rule。 +3. 从 SQL key tuple 中抽取分区键。 +4. 将分区键与 rule 的边界进行比较。 +5. 计算出目标 `partition_id`。 +6. 通过 placement 元数据解析目标 `worker_id`。 + +### Point 操作 + +point `INSERT`、`READ`、`UPDATE`、`DELETE` 都只命中一个 partition。 + +路由时会根据绑定列构造分区键,再按 range rule 计算目标 partition。 + +### Range 操作 + +range read 会做 partition pruning。 + +router 会找出所有与目标范围有交集的 partition。执行层随后: + +- 本地 partition 直接扫描 +- 远端 partition 转发到目标 worker 执行 +- 将返回结果合并 + +## Worker Storage 模型 + +`WorkerStorage` 的 relation 数据按物理 relation identity 管理,而不是按逻辑表管理。 + +也就是说,底层模型从: + +```text +table_id -> relation +``` + +变成: + +```text +(table_id, partition_id) -> relation +``` + +worker 会按需懒创建自己需要访问的 partition relation。 + +## 事务与 WAL 模型 + +要支持 partition write,事务和 WAL 都必须携带物理 partition 身份。 + +当前实现使用统一的 `PhysicalRelationId`: + +```text +{ table_id, partition_id } +``` + +这个标识被用于: + +- 事务暂存 +- 写冲突检查 +- commit 阶段的写锁 +- relation insert/delete 的 WAL 记录 +- WAL replay + +这样可以避免同一张表的不同 partition 在同一 worker 上相互污染。 + +## 远端 Partition 访问 + +当前执行层通过 worker message bus 提供 partition RPC。 + +当前支持的远端操作包括: + +- point read +- range read +- insert +- update +- delete + +当路由命中远端 worker 时,请求会按 partition placement 转发到该 worker 处理。 + +## 当前语义与限制 + +当前实现有明确边界。 + +- 只支持 `RANGE` +- partition binding 预期与主键前缀一致 +- partition pruning 目前只围绕 key 列进行 +- placement 是显式元数据,不是自动调度 +- 远端 partition 访问通过 worker-to-worker RPC 完成 + +目前还存在一个重要限制: + +- 跨 worker 写请求虽然已经可以远程转发执行,但还没有实现分布式两阶段提交 + +这意味着“跨多个 worker 的完整原子提交”目前还不成立。当前模型适合: + +- 单 partition 路由写 +- 跨 partition 读 + +但它还不是完整的分布式事务协议。 + +## 适用场景 + +以下场景适合使用当前 partition 机制: + +- 表天然按有序 key 分布 +- point lookup 和 range scan 都围绕同一组 key 前缀 +- 数据需要分散到多个 worker +- 多张表需要共享同一套逻辑分区布局 + +不要把当前实现直接当成一个已经完整支持跨 worker 原子事务的分布式数据库事务层。 + +## 总结 + +当前 partition 子系统将以下部分解耦: + +- 逻辑分区定义 +- 表绑定 +- worker 放置 +- 物理存储 +- 执行期路由 + +这样的拆分可以保持 schema 模型干净,也为后续扩展打基础,例如: + +- partition rebalance +- partition split / merge +- 分布式 commit 协议 diff --git a/doc/cn/session.cn.md b/doc/cn/session.cn.md index 4e2dd2b..bccf304 100644 --- a/doc/cn/session.cn.md +++ b/doc/cn/session.cn.md @@ -1,12 +1,11 @@ # 系统调用语义 -## 分区与 Worker 标识 +## Worker 标识 -在 `server_ur` 中,每个 worker 都由一个 `partition id` 标识。 +在当前 runtime 路径里,session 路由使用 `worker_id` 表示。 -- 一个 worker 对应一个 partition id -- partition id 是会话本地执行的路由目标 -- 一旦某个 session 绑定到某个 partition,其请求就必须由拥有该 partition 的 worker 处理 +- `worker_id` 是会话本地执行的路由目标 +- 一旦某个 session 绑定到某个 worker,其请求就必须由该 worker 处理 ## Session Open @@ -15,14 +14,14 @@ 该 JSON 负载用于描述 session 路由以及 session 配置变更。负载中至少包含: - `session_id` -- `partition_id` +- `worker_id` 示例: ```json { "session_id": 0, - "partition_id": 3 + "worker_id": 3 } ``` @@ -33,14 +32,14 @@ - 如果 `session_id == 0`,kernel 会创建一个新 session - 如果 `session_id != 0`,则表示该调用引用的是一个已有 session,并修改该 session 的配置 -这里提到的配置变更,指的是同一个 JSON 负载中携带的目标 partition 绑定。 +这里提到的配置变更,指的是同一个 JSON 负载中携带的目标 worker 绑定。 -## `partition_id` 的含义 +## `worker_id` 的含义 -`partition_id` 用于告诉 kernel,哪个 worker 应该拥有该 session。 +`worker_id` 用于告诉 kernel,哪个 worker 应该拥有该 session。 -- 如果当前连接已经附着在拥有该 `partition_id` 的 worker 上,则 session 会在该 worker 上创建或更新 -- 如果当前连接不在该 worker 上,kernel 会将该连接转移到拥有该 `partition_id` 的 worker +- 如果当前连接已经附着在目标 worker 上,则 session 会在该 worker 上创建或更新 +- 如果当前连接不在该 worker 上,kernel 会将该连接转移到该 worker 在这次转移之后,目标 worker 就成为该 session 的拥有者。 @@ -60,10 +59,10 @@ 实际行为如下: 1. 解析传递给 `open` 的可选 JSON 参数。 -2. 读取 `session_id` 和 `partition_id`。 +2. 读取 `session_id` 和 `worker_id`。 3. 如果 `session_id == 0`,则创建一个新 session。 4. 如果 `session_id != 0`,则更新已有 session 的配置。 -5. 确保该 session 由 `partition_id` 指定的 worker 持有。 +5. 确保该 session 由 `worker_id` 指定的 worker 持有。 6. 如有必要,将当前连接转移到该 worker。 7. 在下一次显式修改 session 路由之前,将该 worker 作为当前连接的默认目标 worker。 diff --git a/doc/en/how_to_start.md b/doc/en/how_to_start.md index 24e7731..f9d2514 100644 --- a/doc/en/how_to_start.md +++ b/doc/en/how_to_start.md @@ -81,9 +81,12 @@ python script/build/install_binaries.py --all-workspace-bins Create the configuration file at: ```bash +mkdir -p ${HOME}/.mudu touch ${HOME}/.mudu/mududb_cfg.toml ``` +If the file does not exist, `mudud` also creates `${HOME}/.mudu/mududb_cfg.toml` automatically on first start with default values. + ## Use MuduDB ### 1. Start `mudud` @@ -116,12 +119,12 @@ After `mudud` is running, you can verify the built-in key/value access first, th Each `mcli` command creates and closes its own temporary session automatically, so you do not need to pass a `session_id`. ```bash -mcli put --json '{ +mcli --addr 127.0.0.1:9527 put --json '{ "key": "user-1", "value": "value-1" }' -mcli get --json '{ +mcli --addr 127.0.0.1:9527 get --json '{ "key": "user-1" }' ``` @@ -150,7 +153,7 @@ target/wasm32-wasip2/release/key-value.mpk #### Install the `.mpk` package with mcli ```bash -mcli app-install --mpk target/wasm32-wasip2/release/key-value.mpk +mcli --http-addr 127.0.0.1:8300 app-install --mpk target/wasm32-wasip2/release/key-value.mpk ``` #### Invoke procedures from the installed `.mpk` package @@ -158,7 +161,7 @@ mcli app-install --mpk target/wasm32-wasip2/release/key-value.mpk Insert a record through the `kv_insert` procedure: ```bash -mcli app-invoke --app kv --module key_value --proc kv_insert --json '{ +mcli --addr 127.0.0.1:9527 --http-addr 127.0.0.1:8300 app-invoke --app kv --module key_value --proc kv_insert --json '{ "user_key": "user-1", "value": "value-from-mpk" }' @@ -167,7 +170,7 @@ mcli app-invoke --app kv --module key_value --proc kv_insert --json '{ Read it back through the `kv_read` procedure: ```bash -mcli app-invoke --app kv --module key_value --proc kv_read --json '{ +mcli --addr 127.0.0.1:9527 --http-addr 127.0.0.1:8300 app-invoke --app kv --module key_value --proc kv_read --json '{ "user_key": "user-1" }' ``` diff --git a/doc/en/partition.md b/doc/en/partition.md new file mode 100644 index 0000000..8ec1abd --- /dev/null +++ b/doc/en/partition.md @@ -0,0 +1,270 @@ +# Partitioning + +This document describes the current partition mechanism in MuduDB. + +The implementation is based on four layers: + +- a global range partition rule +- a table-to-rule binding +- a partition-to-worker placement +- a routing layer that maps SQL operations to physical partitions + +The goal is to let multiple tables share the same range layout while allowing partitions to be distributed across +multiple workers. + +## Core Concepts + +### Partition Rule + +A partition rule is a global metadata object. + +It defines: + +- the partitioning method, currently `RANGE` +- one or more partition key columns +- an ordered set of partition boundaries +- a list of logical partitions + +Each rule produces multiple logical partitions. These partitions are later assigned to workers by placement metadata. + +### Table Binding + +A table does not store the full partition layout in its schema. + +Instead, a table binds to an existing global rule and declares which table columns reference that rule. + +This lets multiple tables reuse the same partition layout. + +In the current implementation, the referenced columns are expected to match the primary-key prefix. This keeps point +lookup, range pruning, and storage routing aligned with the existing key encoding model. + +### Placement + +Placement maps each logical partition to a worker. + +This is a separate metadata layer because: + +- placement is a deployment concern, not a schema concern +- placement may change independently of the table definition +- the same rule may be reused by multiple tables + +### Physical Relation + +Inside a worker, relation storage is not keyed only by `table_id`. + +It is keyed by the physical pair: + +```text +(table_id, partition_id) +``` + +This is required because one worker may own multiple partitions of the same logical table. + +## Metadata Model + +The partition metadata is split into three object types. + +### `PartitionRuleDesc` + +Defines a global range rule: + +- `rule_id` +- `name` +- `kind` +- `key_types` +- `partitions` +- `version` + +Each partition entry contains: + +- `partition_id` +- `name` +- `start` bound, inclusive +- `end` bound, exclusive + +### `TablePartitionBinding` + +Defines how a table uses a global rule: + +- `table_id` +- `rule_id` +- `ref_attr_indices` + +`ref_attr_indices` identifies the table columns that form the partition key. + +### `PartitionPlacement` + +Defines where a partition lives: + +- `partition_id` +- `worker_id` + +## Catalogs + +Partition metadata is persisted in dedicated internal catalogs. + +- `__meta_partition_rule` +- `__meta_table_partition_binding` +- `__meta_partition_placement` + +These catalogs are managed by `MetaMgr` and cached in memory by the metadata layer. + +## DDL + +The current implementation supports the following statements. + +### Create a Global Partition Rule + +```sql +CREATE PARTITION RULE r_orders +RANGE (region_id, order_id) ( + PARTITION p0 VALUES FROM (MINVALUE, MINVALUE) TO (1000, MINVALUE), + PARTITION p1 VALUES FROM (1000, MINVALUE) TO (2000, MINVALUE), + PARTITION p2 VALUES FROM (2000, MINVALUE) TO (MAXVALUE, MAXVALUE) +); +``` + +### Create a Partitioned Table + +```sql +CREATE TABLE orders ( + region_id BIGINT, + order_id BIGINT, + amount BIGINT, + PRIMARY KEY(region_id, order_id) +) +PARTITION BY GLOBAL RULE r_orders +REFERENCES (region_id, order_id); +``` + +### Create Partition Placement + +```sql +CREATE PARTITION PLACEMENT FOR RULE r_orders ( + PARTITION p0 ON WORKER 1, + PARTITION p1 ON WORKER 2, + PARTITION p2 ON WORKER 3 +); +``` + +## Routing Model + +Routing is implemented by `PartitionRouter`. + +For a partitioned table, the router performs these steps: + +1. Load the table binding. +2. Load the referenced partition rule. +3. Extract the partition key from the SQL key tuple. +4. Compare the key tuple with partition bounds. +5. Resolve the target `partition_id`. +6. Resolve the target `worker_id` from placement metadata. + +### Point Operations + +Point `INSERT`, `READ`, `UPDATE`, and `DELETE` are routed to one partition. + +The routing key is built from the bound partition columns, then compared to the range rule. + +### Range Operations + +Range reads perform partition pruning. + +The router finds all partitions whose ranges overlap the requested key range. The engine then: + +- scans matching local partitions directly +- forwards requests for remote partitions to the owning worker +- merges the returned rows + +## Worker Storage Model + +`WorkerStorage` stores relation data by physical relation identity rather than logical table identity. + +This changes the storage model from: + +```text +table_id -> relation +``` + +to: + +```text +(table_id, partition_id) -> relation +``` + +Relations are created lazily for the partitions that the worker needs to access. + +## Transaction and WAL Model + +Partitioned writes require transaction state and WAL to carry physical partition identity. + +The current implementation uses a shared `PhysicalRelationId`: + +```text +{ table_id, partition_id } +``` + +This identity is used by: + +- transaction staging +- write conflict detection +- commit-time write locking +- WAL records for relation insert and delete +- WAL replay + +This avoids corrupting data when multiple partitions of the same table exist on the same worker. + +## Remote Partition Access + +The engine currently supports partition RPC over the worker message bus. + +Supported remote actions: + +- point read +- range read +- insert +- update +- delete + +Remote requests are routed by partition placement and executed by the worker that owns the target partition. + +## Current Semantics and Limits + +The current implementation is intentionally scoped. + +- only `RANGE` partitioning is supported +- partition bindings are expected to match the primary-key prefix +- partition pruning is based on key columns, not arbitrary predicates +- placement is explicit metadata +- remote partition access uses worker-to-worker RPC + +There is still an important transactional limit: + +- cross-worker writes are forwarded and executed remotely, but there is no distributed two-phase commit yet + +This means full atomic commit across multiple workers is not implemented. The current model is suitable for routed +single-partition writes and for cross-partition reads, but it is not yet a complete distributed transaction protocol. + +## Recommended Usage + +Use partitioning when: + +- tables are naturally partitioned by ordered keys +- point lookups and range scans follow the same key prefix +- data should be spread across multiple workers +- a single logical partition layout should be reused by multiple tables + +Avoid using the current implementation as if it were already a general distributed transaction engine. + +## Summary + +The partition subsystem separates: + +- logical partition definition +- table binding +- worker placement +- physical storage +- execution-time routing + +This keeps the schema model clean and allows the engine to evolve toward partition rebalance, partition split or merge, +and distributed commit in later iterations. diff --git a/doc/en/session.md b/doc/en/session.md index b9d5046..44aa54e 100644 --- a/doc/en/session.md +++ b/doc/en/session.md @@ -1,12 +1,11 @@ # Syscall Semantics -## Partition and Worker Identity +## Worker Identity -In `server_ur`, each worker is identified by a `partition id`. +In the current runtime path, session routing is expressed in terms of `worker_id`. -- one worker corresponds to one partition id -- partition id is the routing target for session-local execution -- once a session is bound to a partition, its requests must be handled by the worker that owns that partition +- `worker_id` is the routing target for session-local execution +- once a session is bound to a worker, its requests must be handled by that worker ## Session Open @@ -15,14 +14,14 @@ In `server_ur`, each worker is identified by a `partition id`. The JSON payload is used to describe session routing and session configuration changes. The payload contains at least: - `session_id` -- `partition_id` +- `worker_id` Example: ```json { "session_id": 0, - "partition_id": 3 + "worker_id": 3 } ``` @@ -33,14 +32,14 @@ Example: - if `session_id == 0`, the kernel creates a new session - if `session_id != 0`, the call refers to an existing session and changes that session's configuration -The configuration change described here is the target partition binding carried by the same JSON payload. +The configuration change described here is the target worker binding carried by the same JSON payload. -## `partition_id` Meaning +## `worker_id` Meaning -`partition_id` tells the kernel which worker should own the session. +`worker_id` tells the kernel which worker should own the session. -- if the current connection is already attached to the worker that owns `partition_id`, the session is created or updated there -- if the current connection is not attached to that worker, the kernel transfers the connection to the worker that owns `partition_id` +- if the current connection is already attached to the target worker, the session is created or updated there +- if the current connection is not attached to that worker, the kernel transfers the connection to that worker After this transfer, the target worker becomes the owner of that session. @@ -60,10 +59,10 @@ This default stays in effect until another session on the same connection explic The effective behavior is: 1. Parse the optional JSON argument passed to `open`. -2. Read `session_id` and `partition_id`. +2. Read `session_id` and `worker_id`. 3. If `session_id == 0`, create a new session. 4. If `session_id != 0`, update the existing session configuration. -5. Ensure the session is owned by the worker identified by `partition_id`. +5. Ensure the session is owned by the worker identified by `worker_id`. 6. If necessary, transfer the current connection to that worker. 7. Use that worker as the default connection target until another explicit session routing change happens. diff --git a/doc/en/syscall.md b/doc/en/syscall.md index 4e6c41c..bfe4181 100644 --- a/doc/en/syscall.md +++ b/doc/en/syscall.md @@ -120,8 +120,13 @@ pub async fn mudu_command(oid: OID, sql: &dyn SQLStmt, params: &dyn SQLParams) - ### 3. `batch` -`batch` executes a batch SQL string through the batch syscall path. It currently targets libsql-backed runtime -sessions and reuses the same serialized argument and return format as `command`. +`batch` executes a batch SQL string through the batch syscall path. In the current host implementation, it is available for the SQLite, PostgreSQL, and MySQL standalone adapter paths, and reuses the same serialized argument and return format as `command`. + +Current limitations: + +- `batch` does not support SQL parameters +- the `mudud` adapter path still returns `NotImplemented` +- portable examples should still prefer executing schema setup statement-by-statement through `command` when they need to run unchanged across all adapters &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static ARGV_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - ARGV_DESC.get_or_init(|| { - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "user_id".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "name".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "email".to_string(), - ::dat_type().clone(), - ), - ]) - }) +pub fn mudu_argv_desc_deposit() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static ARGV_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + ARGV_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "user_id".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "amount".to_string(), + + ::dat_type().clone() + + ), + + ]) + } + ) } -pub fn mudu_result_desc_create_user() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static RESULT_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - RESULT_DESC - .get_or_init(|| ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![])) +pub fn mudu_result_desc_deposit() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static RESULT_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + RESULT_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ]) + } + ) } -pub fn mudu_proc_desc_create_user() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { - static _PROC_DESC: std::sync::OnceLock<::mudu_contract::procedure::proc_desc::ProcDesc> = - std::sync::OnceLock::new(); - _PROC_DESC.get_or_init(|| { - ::mudu_contract::procedure::proc_desc::ProcDesc::new( - "wallet".to_string(), - "create_user".to_string(), - mudu_argv_desc_create_user().clone(), - mudu_result_desc_create_user().clone(), - false, - ) - }) +pub fn mudu_proc_desc_deposit() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { + static _PROC_DESC: std::sync::OnceLock< + ::mudu_contract::procedure::proc_desc::ProcDesc, + > = std::sync::OnceLock::new(); + _PROC_DESC + .get_or_init(|| { + ::mudu_contract::procedure::proc_desc::ProcDesc::new( + "wallet".to_string(), + "deposit".to_string(), + mudu_argv_desc_deposit().clone(), + mudu_result_desc_deposit().clone(), + false + ) + }) } -mod mod_create_user { +mod mod_deposit { wit_bindgen::generate!({ inline: - r##"package mudu:mp2-create-user; - world mudu-app-mp2-create-user { - export mp2-create-user: func(param:list) -> list; + r##"package mudu:mp2-deposit; + world mudu-app-mp2-deposit { + export mp2-deposit: func(param:list) -> list; } "##, async: true @@ -494,96 +518,134 @@ mod mod_create_user { #[allow(non_camel_case_types)] #[allow(unused)] - struct GuestCreateUser {} + struct GuestDeposit {} - impl Guest for GuestCreateUser { - async fn mp2_create_user(param: Vec) -> Vec { - super::mp2_create_user(param).await + impl Guest for GuestDeposit { + async fn mp2_deposit(param:Vec) -> Vec { + super::mp2_deposit(param).await } } - export!(GuestCreateUser); + export!(GuestDeposit); } -async fn mp2_purchase(param: Vec) -> Vec { +async fn mp2_transfer_funds(param:Vec) -> Vec { ::mudu_binding::procedure::procedure_invoke::invoke_procedure_async( param, - mudu_inner_p2_purchase, - ) - .await + mudu_inner_p2_transfer_funds, + ).await } -pub async fn mudu_inner_p2_purchase( +pub async fn mudu_inner_p2_transfer_funds( param: ::mudu_contract::procedure::procedure_param::ProcedureParam, -) -> ::mudu::common::result::RS<::mudu_contract::procedure::procedure_result::ProcedureResult> { - let return_desc = mudu_result_desc_purchase().clone(); - let res = purchase( +) -> ::mudu::common::result::RS< + ::mudu_contract::procedure::procedure_result::ProcedureResult, +> { + let return_desc = mudu_result_desc_transfer_funds().clone(); + let res = transfer_funds( param.session_id(), - ::mudu_type::datum::value_to_typed::(¶m.param_list()[0], "i32")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[1], "i32")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[2], "String")?, - ) - .await; + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[0], "i32")?, + + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[1], "i32")?, + + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[2], "i32")?, + + + ).await; match res { Ok(tuple) => { - let return_list = { vec![] }; + let return_list = { + + vec![] + + }; Ok(::mudu_contract::procedure::procedure_result::ProcedureResult::new(return_list)) } Err(e) => Err(e), } } -pub fn mudu_argv_desc_purchase() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc -{ - static ARGV_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - ARGV_DESC.get_or_init(|| { - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "user_id".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "amount".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "description".to_string(), - ::dat_type().clone(), - ), - ]) - }) +pub fn mudu_argv_desc_transfer_funds() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static ARGV_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + ARGV_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "from_user_id".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "to_user_id".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "amount".to_string(), + + ::dat_type().clone() + + ), + + ]) + } + ) } -pub fn mudu_result_desc_purchase() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static RESULT_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - RESULT_DESC - .get_or_init(|| ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![])) +pub fn mudu_result_desc_transfer_funds() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static RESULT_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + RESULT_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ]) + } + ) } -pub fn mudu_proc_desc_purchase() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { - static _PROC_DESC: std::sync::OnceLock<::mudu_contract::procedure::proc_desc::ProcDesc> = - std::sync::OnceLock::new(); - _PROC_DESC.get_or_init(|| { - ::mudu_contract::procedure::proc_desc::ProcDesc::new( - "wallet".to_string(), - "purchase".to_string(), - mudu_argv_desc_purchase().clone(), - mudu_result_desc_purchase().clone(), - false, - ) - }) +pub fn mudu_proc_desc_transfer_funds() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { + static _PROC_DESC: std::sync::OnceLock< + ::mudu_contract::procedure::proc_desc::ProcDesc, + > = std::sync::OnceLock::new(); + _PROC_DESC + .get_or_init(|| { + ::mudu_contract::procedure::proc_desc::ProcDesc::new( + "wallet".to_string(), + "transfer_funds".to_string(), + mudu_argv_desc_transfer_funds().clone(), + mudu_result_desc_transfer_funds().clone(), + false + ) + }) } -mod mod_purchase { +mod mod_transfer_funds { wit_bindgen::generate!({ inline: - r##"package mudu:mp2-purchase; - world mudu-app-mp2-purchase { - export mp2-purchase: func(param:list) -> list; + r##"package mudu:mp2-transfer-funds; + world mudu-app-mp2-transfer-funds { + export mp2-transfer-funds: func(param:list) -> list; } "##, async: true @@ -591,78 +653,98 @@ mod mod_purchase { #[allow(non_camel_case_types)] #[allow(unused)] - struct GuestPurchase {} + struct GuestTransferFunds {} - impl Guest for GuestPurchase { - async fn mp2_purchase(param: Vec) -> Vec { - super::mp2_purchase(param).await + impl Guest for GuestTransferFunds { + async fn mp2_transfer_funds(param:Vec) -> Vec { + super::mp2_transfer_funds(param).await } } - export!(GuestPurchase); + export!(GuestTransferFunds); } -async fn mp2_delete_user(param: Vec) -> Vec { +async fn mp2_delete_user(param:Vec) -> Vec { ::mudu_binding::procedure::procedure_invoke::invoke_procedure_async( param, mudu_inner_p2_delete_user, - ) - .await + ).await } pub async fn mudu_inner_p2_delete_user( param: ::mudu_contract::procedure::procedure_param::ProcedureParam, -) -> ::mudu::common::result::RS<::mudu_contract::procedure::procedure_result::ProcedureResult> { +) -> ::mudu::common::result::RS< + ::mudu_contract::procedure::procedure_result::ProcedureResult, +> { let return_desc = mudu_result_desc_delete_user().clone(); let res = delete_user( param.session_id(), - ::mudu_type::datum::value_to_typed::(¶m.param_list()[0], "i32")?, - ) - .await; + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[0], "i32")?, + + + ).await; match res { Ok(tuple) => { - let return_list = { vec![] }; + let return_list = { + + vec![] + + }; Ok(::mudu_contract::procedure::procedure_result::ProcedureResult::new(return_list)) } Err(e) => Err(e), } } -pub fn mudu_argv_desc_delete_user() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static ARGV_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - ARGV_DESC.get_or_init(|| { - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "user_id".to_string(), - ::dat_type().clone(), - ), - ]) - }) +pub fn mudu_argv_desc_delete_user() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static ARGV_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + ARGV_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "user_id".to_string(), + + ::dat_type().clone() + + ), + + ]) + } + ) } -pub fn mudu_result_desc_delete_user() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static RESULT_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - RESULT_DESC - .get_or_init(|| ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![])) +pub fn mudu_result_desc_delete_user() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static RESULT_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + RESULT_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ]) + } + ) } -pub fn mudu_proc_desc_delete_user() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { - static _PROC_DESC: std::sync::OnceLock<::mudu_contract::procedure::proc_desc::ProcDesc> = - std::sync::OnceLock::new(); - _PROC_DESC.get_or_init(|| { - ::mudu_contract::procedure::proc_desc::ProcDesc::new( - "wallet".to_string(), - "delete_user".to_string(), - mudu_argv_desc_delete_user().clone(), - mudu_result_desc_delete_user().clone(), - false, - ) - }) +pub fn mudu_proc_desc_delete_user() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { + static _PROC_DESC: std::sync::OnceLock< + ::mudu_contract::procedure::proc_desc::ProcDesc, + > = std::sync::OnceLock::new(); + _PROC_DESC + .get_or_init(|| { + ::mudu_contract::procedure::proc_desc::ProcDesc::new( + "wallet".to_string(), + "delete_user".to_string(), + mudu_argv_desc_delete_user().clone(), + mudu_result_desc_delete_user().clone(), + false + ) + }) } mod mod_delete_user { @@ -681,88 +763,117 @@ mod mod_delete_user { struct GuestDeleteUser {} impl Guest for GuestDeleteUser { - async fn mp2_delete_user(param: Vec) -> Vec { + async fn mp2_delete_user(param:Vec) -> Vec { super::mp2_delete_user(param).await } } export!(GuestDeleteUser); } -async fn mp2_deposit(param: Vec) -> Vec { +async fn mp2_withdraw(param:Vec) -> Vec { ::mudu_binding::procedure::procedure_invoke::invoke_procedure_async( param, - mudu_inner_p2_deposit, - ) - .await + mudu_inner_p2_withdraw, + ).await } -pub async fn mudu_inner_p2_deposit( +pub async fn mudu_inner_p2_withdraw( param: ::mudu_contract::procedure::procedure_param::ProcedureParam, -) -> ::mudu::common::result::RS<::mudu_contract::procedure::procedure_result::ProcedureResult> { - let return_desc = mudu_result_desc_deposit().clone(); - let res = deposit( +) -> ::mudu::common::result::RS< + ::mudu_contract::procedure::procedure_result::ProcedureResult, +> { + let return_desc = mudu_result_desc_withdraw().clone(); + let res = withdraw( param.session_id(), - ::mudu_type::datum::value_to_typed::(¶m.param_list()[0], "i32")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[1], "i32")?, - ) - .await; + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[0], "i32")?, + + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[1], "i32")?, + + + ).await; match res { Ok(tuple) => { - let return_list = { vec![] }; + let return_list = { + + vec![] + + }; Ok(::mudu_contract::procedure::procedure_result::ProcedureResult::new(return_list)) } Err(e) => Err(e), } } -pub fn mudu_argv_desc_deposit() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc -{ - static ARGV_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - ARGV_DESC.get_or_init(|| { - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "user_id".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "amount".to_string(), - ::dat_type().clone(), - ), - ]) - }) +pub fn mudu_argv_desc_withdraw() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static ARGV_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + ARGV_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "user_id".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "amount".to_string(), + + ::dat_type().clone() + + ), + + ]) + } + ) } -pub fn mudu_result_desc_deposit() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static RESULT_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - RESULT_DESC - .get_or_init(|| ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![])) +pub fn mudu_result_desc_withdraw() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static RESULT_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + RESULT_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ]) + } + ) } -pub fn mudu_proc_desc_deposit() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { - static _PROC_DESC: std::sync::OnceLock<::mudu_contract::procedure::proc_desc::ProcDesc> = - std::sync::OnceLock::new(); - _PROC_DESC.get_or_init(|| { - ::mudu_contract::procedure::proc_desc::ProcDesc::new( - "wallet".to_string(), - "deposit".to_string(), - mudu_argv_desc_deposit().clone(), - mudu_result_desc_deposit().clone(), - false, - ) - }) +pub fn mudu_proc_desc_withdraw() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { + static _PROC_DESC: std::sync::OnceLock< + ::mudu_contract::procedure::proc_desc::ProcDesc, + > = std::sync::OnceLock::new(); + _PROC_DESC + .get_or_init(|| { + ::mudu_contract::procedure::proc_desc::ProcDesc::new( + "wallet".to_string(), + "withdraw".to_string(), + mudu_argv_desc_withdraw().clone(), + mudu_result_desc_withdraw().clone(), + false + ) + }) } -mod mod_deposit { +mod mod_withdraw { wit_bindgen::generate!({ inline: - r##"package mudu:mp2-deposit; - world mudu-app-mp2-deposit { - export mp2-deposit: func(param:list) -> list; + r##"package mudu:mp2-withdraw; + world mudu-app-mp2-withdraw { + export mp2-withdraw: func(param:list) -> list; } "##, async: true @@ -770,96 +881,134 @@ mod mod_deposit { #[allow(non_camel_case_types)] #[allow(unused)] - struct GuestDeposit {} + struct GuestWithdraw {} - impl Guest for GuestDeposit { - async fn mp2_deposit(param: Vec) -> Vec { - super::mp2_deposit(param).await + impl Guest for GuestWithdraw { + async fn mp2_withdraw(param:Vec) -> Vec { + super::mp2_withdraw(param).await } } - export!(GuestDeposit); + export!(GuestWithdraw); } -async fn mp2_transfer_funds(param: Vec) -> Vec { +async fn mp2_purchase(param:Vec) -> Vec { ::mudu_binding::procedure::procedure_invoke::invoke_procedure_async( param, - mudu_inner_p2_transfer_funds, - ) - .await + mudu_inner_p2_purchase, + ).await } -pub async fn mudu_inner_p2_transfer_funds( +pub async fn mudu_inner_p2_purchase( param: ::mudu_contract::procedure::procedure_param::ProcedureParam, -) -> ::mudu::common::result::RS<::mudu_contract::procedure::procedure_result::ProcedureResult> { - let return_desc = mudu_result_desc_transfer_funds().clone(); - let res = transfer_funds( +) -> ::mudu::common::result::RS< + ::mudu_contract::procedure::procedure_result::ProcedureResult, +> { + let return_desc = mudu_result_desc_purchase().clone(); + let res = purchase( param.session_id(), - ::mudu_type::datum::value_to_typed::(¶m.param_list()[0], "i32")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[1], "i32")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[2], "i32")?, - ) - .await; + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[0], "i32")?, + + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[1], "i32")?, + + + + ::mudu_type::datum::value_to_typed::< + String, + _, + >(¶m.param_list()[2], "String")?, + + + ).await; match res { Ok(tuple) => { - let return_list = { vec![] }; + let return_list = { + + vec![] + + }; Ok(::mudu_contract::procedure::procedure_result::ProcedureResult::new(return_list)) } Err(e) => Err(e), } } -pub fn mudu_argv_desc_transfer_funds() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static ARGV_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - ARGV_DESC.get_or_init(|| { - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "from_user_id".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "to_user_id".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "amount".to_string(), - ::dat_type().clone(), - ), - ]) - }) +pub fn mudu_argv_desc_purchase() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static ARGV_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + ARGV_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "user_id".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "amount".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "description".to_string(), + + ::dat_type().clone() + + ), + + ]) + } + ) } -pub fn mudu_result_desc_transfer_funds() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static RESULT_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - RESULT_DESC - .get_or_init(|| ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![])) +pub fn mudu_result_desc_purchase() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static RESULT_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + RESULT_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ]) + } + ) } -pub fn mudu_proc_desc_transfer_funds() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { - static _PROC_DESC: std::sync::OnceLock<::mudu_contract::procedure::proc_desc::ProcDesc> = - std::sync::OnceLock::new(); - _PROC_DESC.get_or_init(|| { - ::mudu_contract::procedure::proc_desc::ProcDesc::new( - "wallet".to_string(), - "transfer_funds".to_string(), - mudu_argv_desc_transfer_funds().clone(), - mudu_result_desc_transfer_funds().clone(), - false, - ) - }) +pub fn mudu_proc_desc_purchase() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { + static _PROC_DESC: std::sync::OnceLock< + ::mudu_contract::procedure::proc_desc::ProcDesc, + > = std::sync::OnceLock::new(); + _PROC_DESC + .get_or_init(|| { + ::mudu_contract::procedure::proc_desc::ProcDesc::new( + "wallet".to_string(), + "purchase".to_string(), + mudu_argv_desc_purchase().clone(), + mudu_result_desc_purchase().clone(), + false + ) + }) } -mod mod_transfer_funds { +mod mod_purchase { wit_bindgen::generate!({ inline: - r##"package mudu:mp2-transfer-funds; - world mudu-app-mp2-transfer-funds { - export mp2-transfer-funds: func(param:list) -> list; + r##"package mudu:mp2-purchase; + world mudu-app-mp2-purchase { + export mp2-purchase: func(param:list) -> list; } "##, async: true @@ -867,96 +1016,134 @@ mod mod_transfer_funds { #[allow(non_camel_case_types)] #[allow(unused)] - struct GuestTransferFunds {} + struct GuestPurchase {} - impl Guest for GuestTransferFunds { - async fn mp2_transfer_funds(param: Vec) -> Vec { - super::mp2_transfer_funds(param).await + impl Guest for GuestPurchase { + async fn mp2_purchase(param:Vec) -> Vec { + super::mp2_purchase(param).await } } - export!(GuestTransferFunds); + export!(GuestPurchase); } -async fn mp2_update_user(param: Vec) -> Vec { +async fn mp2_create_user(param:Vec) -> Vec { ::mudu_binding::procedure::procedure_invoke::invoke_procedure_async( param, - mudu_inner_p2_update_user, - ) - .await + mudu_inner_p2_create_user, + ).await } -pub async fn mudu_inner_p2_update_user( +pub async fn mudu_inner_p2_create_user( param: ::mudu_contract::procedure::procedure_param::ProcedureParam, -) -> ::mudu::common::result::RS<::mudu_contract::procedure::procedure_result::ProcedureResult> { - let return_desc = mudu_result_desc_update_user().clone(); - let res = update_user( +) -> ::mudu::common::result::RS< + ::mudu_contract::procedure::procedure_result::ProcedureResult, +> { + let return_desc = mudu_result_desc_create_user().clone(); + let res = create_user( param.session_id(), - ::mudu_type::datum::value_to_typed::(¶m.param_list()[0], "i32")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[1], "String")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[2], "String")?, - ) - .await; + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[0], "i32")?, + + + + ::mudu_type::datum::value_to_typed::< + String, + _, + >(¶m.param_list()[1], "String")?, + + + + ::mudu_type::datum::value_to_typed::< + String, + _, + >(¶m.param_list()[2], "String")?, + + + ).await; match res { Ok(tuple) => { - let return_list = { vec![] }; + let return_list = { + + vec![] + + }; Ok(::mudu_contract::procedure::procedure_result::ProcedureResult::new(return_list)) } Err(e) => Err(e), } } -pub fn mudu_argv_desc_update_user() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static ARGV_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - ARGV_DESC.get_or_init(|| { - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "user_id".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "name".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "email".to_string(), - ::dat_type().clone(), - ), - ]) - }) +pub fn mudu_argv_desc_create_user() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static ARGV_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + ARGV_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "user_id".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "name".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "email".to_string(), + + ::dat_type().clone() + + ), + + ]) + } + ) } -pub fn mudu_result_desc_update_user() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static RESULT_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - RESULT_DESC - .get_or_init(|| ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![])) +pub fn mudu_result_desc_create_user() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static RESULT_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + RESULT_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ]) + } + ) } -pub fn mudu_proc_desc_update_user() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { - static _PROC_DESC: std::sync::OnceLock<::mudu_contract::procedure::proc_desc::ProcDesc> = - std::sync::OnceLock::new(); - _PROC_DESC.get_or_init(|| { - ::mudu_contract::procedure::proc_desc::ProcDesc::new( - "wallet".to_string(), - "update_user".to_string(), - mudu_argv_desc_update_user().clone(), - mudu_result_desc_update_user().clone(), - false, - ) - }) +pub fn mudu_proc_desc_create_user() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { + static _PROC_DESC: std::sync::OnceLock< + ::mudu_contract::procedure::proc_desc::ProcDesc, + > = std::sync::OnceLock::new(); + _PROC_DESC + .get_or_init(|| { + ::mudu_contract::procedure::proc_desc::ProcDesc::new( + "wallet".to_string(), + "create_user".to_string(), + mudu_argv_desc_create_user().clone(), + mudu_result_desc_create_user().clone(), + false + ) + }) } -mod mod_update_user { +mod mod_create_user { wit_bindgen::generate!({ inline: - r##"package mudu:mp2-update-user; - world mudu-app-mp2-update-user { - export mp2-update-user: func(param:list) -> list; + r##"package mudu:mp2-create-user; + world mudu-app-mp2-create-user { + export mp2-create-user: func(param:list) -> list; } "##, async: true @@ -964,91 +1151,134 @@ mod mod_update_user { #[allow(non_camel_case_types)] #[allow(unused)] - struct GuestUpdateUser {} + struct GuestCreateUser {} - impl Guest for GuestUpdateUser { - async fn mp2_update_user(param: Vec) -> Vec { - super::mp2_update_user(param).await + impl Guest for GuestCreateUser { + async fn mp2_create_user(param:Vec) -> Vec { + super::mp2_create_user(param).await } } - export!(GuestUpdateUser); + export!(GuestCreateUser); } -async fn mp2_withdraw(param: Vec) -> Vec { +async fn mp2_update_user(param:Vec) -> Vec { ::mudu_binding::procedure::procedure_invoke::invoke_procedure_async( param, - mudu_inner_p2_withdraw, - ) - .await + mudu_inner_p2_update_user, + ).await } -pub async fn mudu_inner_p2_withdraw( +pub async fn mudu_inner_p2_update_user( param: ::mudu_contract::procedure::procedure_param::ProcedureParam, -) -> ::mudu::common::result::RS<::mudu_contract::procedure::procedure_result::ProcedureResult> { - let return_desc = mudu_result_desc_withdraw().clone(); - let res = withdraw( +) -> ::mudu::common::result::RS< + ::mudu_contract::procedure::procedure_result::ProcedureResult, +> { + let return_desc = mudu_result_desc_update_user().clone(); + let res = update_user( param.session_id(), - ::mudu_type::datum::value_to_typed::(¶m.param_list()[0], "i32")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[1], "i32")?, - ) - .await; + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[0], "i32")?, + + + + ::mudu_type::datum::value_to_typed::< + String, + _, + >(¶m.param_list()[1], "String")?, + + + + ::mudu_type::datum::value_to_typed::< + String, + _, + >(¶m.param_list()[2], "String")?, + + + ).await; match res { Ok(tuple) => { - let return_list = { vec![] }; + let return_list = { + + vec![] + + }; Ok(::mudu_contract::procedure::procedure_result::ProcedureResult::new(return_list)) } Err(e) => Err(e), } } -pub fn mudu_argv_desc_withdraw() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc -{ - static ARGV_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - ARGV_DESC.get_or_init(|| { - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "user_id".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "amount".to_string(), - ::dat_type().clone(), - ), - ]) - }) +pub fn mudu_argv_desc_update_user() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static ARGV_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + ARGV_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "user_id".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "name".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "email".to_string(), + + ::dat_type().clone() + + ), + + ]) + } + ) } -pub fn mudu_result_desc_withdraw() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static RESULT_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - RESULT_DESC - .get_or_init(|| ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![])) +pub fn mudu_result_desc_update_user() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static RESULT_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + RESULT_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ]) + } + ) } -pub fn mudu_proc_desc_withdraw() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { - static _PROC_DESC: std::sync::OnceLock<::mudu_contract::procedure::proc_desc::ProcDesc> = - std::sync::OnceLock::new(); - _PROC_DESC.get_or_init(|| { - ::mudu_contract::procedure::proc_desc::ProcDesc::new( - "wallet".to_string(), - "withdraw".to_string(), - mudu_argv_desc_withdraw().clone(), - mudu_result_desc_withdraw().clone(), - false, - ) - }) +pub fn mudu_proc_desc_update_user() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { + static _PROC_DESC: std::sync::OnceLock< + ::mudu_contract::procedure::proc_desc::ProcDesc, + > = std::sync::OnceLock::new(); + _PROC_DESC + .get_or_init(|| { + ::mudu_contract::procedure::proc_desc::ProcDesc::new( + "wallet".to_string(), + "update_user".to_string(), + mudu_argv_desc_update_user().clone(), + mudu_result_desc_update_user().clone(), + false + ) + }) } -mod mod_withdraw { +mod mod_update_user { wit_bindgen::generate!({ inline: - r##"package mudu:mp2-withdraw; - world mudu-app-mp2-withdraw { - export mp2-withdraw: func(param:list) -> list; + r##"package mudu:mp2-update-user; + world mudu-app-mp2-update-user { + export mp2-update-user: func(param:list) -> list; } "##, async: true @@ -1056,88 +1286,126 @@ mod mod_withdraw { #[allow(non_camel_case_types)] #[allow(unused)] - struct GuestWithdraw {} + struct GuestUpdateUser {} - impl Guest for GuestWithdraw { - async fn mp2_withdraw(param: Vec) -> Vec { - super::mp2_withdraw(param).await + impl Guest for GuestUpdateUser { + async fn mp2_update_user(param:Vec) -> Vec { + super::mp2_update_user(param).await } } - export!(GuestWithdraw); + export!(GuestUpdateUser); } -async fn mp2_transfer(param: Vec) -> Vec { +async fn mp2_transfer(param:Vec) -> Vec { ::mudu_binding::procedure::procedure_invoke::invoke_procedure_async( param, mudu_inner_p2_transfer, - ) - .await + ).await } pub async fn mudu_inner_p2_transfer( param: ::mudu_contract::procedure::procedure_param::ProcedureParam, -) -> ::mudu::common::result::RS<::mudu_contract::procedure::procedure_result::ProcedureResult> { +) -> ::mudu::common::result::RS< + ::mudu_contract::procedure::procedure_result::ProcedureResult, +> { let return_desc = mudu_result_desc_transfer().clone(); let res = transfer( param.session_id(), - ::mudu_type::datum::value_to_typed::(¶m.param_list()[0], "i32")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[1], "i32")?, - ::mudu_type::datum::value_to_typed::(¶m.param_list()[2], "i32")?, - ) - .await; + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[0], "i32")?, + + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[1], "i32")?, + + + + ::mudu_type::datum::value_to_typed::< + i32, + _, + >(¶m.param_list()[2], "i32")?, + + + ).await; match res { Ok(tuple) => { - let return_list = { vec![] }; + let return_list = { + + vec![] + + }; Ok(::mudu_contract::procedure::procedure_result::ProcedureResult::new(return_list)) } Err(e) => Err(e), } } -pub fn mudu_argv_desc_transfer() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc -{ - static ARGV_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - ARGV_DESC.get_or_init(|| { - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "from_user_id".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "to_user_id".to_string(), - ::dat_type().clone(), - ), - ::mudu_contract::tuple::datum_desc::DatumDesc::new( - "amount".to_string(), - ::dat_type().clone(), - ), - ]) - }) +pub fn mudu_argv_desc_transfer() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static ARGV_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + ARGV_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "from_user_id".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "to_user_id".to_string(), + + ::dat_type().clone() + + ), + + ::mudu_contract::tuple::datum_desc::DatumDesc::new( + "amount".to_string(), + + ::dat_type().clone() + + ), + + ]) + } + ) } -pub fn mudu_result_desc_transfer() --> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { - static RESULT_DESC: std::sync::OnceLock< - ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc, - > = std::sync::OnceLock::new(); - RESULT_DESC - .get_or_init(|| ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![])) +pub fn mudu_result_desc_transfer() -> &'static ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc { + static RESULT_DESC: std::sync::OnceLock<::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc> = + std::sync::OnceLock::new(); + RESULT_DESC.get_or_init(|| + { + ::mudu_contract::tuple::tuple_field_desc::TupleFieldDesc::new(vec![ + + ]) + } + ) } -pub fn mudu_proc_desc_transfer() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { - static _PROC_DESC: std::sync::OnceLock<::mudu_contract::procedure::proc_desc::ProcDesc> = - std::sync::OnceLock::new(); - _PROC_DESC.get_or_init(|| { - ::mudu_contract::procedure::proc_desc::ProcDesc::new( - "wallet".to_string(), - "transfer".to_string(), - mudu_argv_desc_transfer().clone(), - mudu_result_desc_transfer().clone(), - false, - ) - }) +pub fn mudu_proc_desc_transfer() -> &'static ::mudu_contract::procedure::proc_desc::ProcDesc { + static _PROC_DESC: std::sync::OnceLock< + ::mudu_contract::procedure::proc_desc::ProcDesc, + > = std::sync::OnceLock::new(); + _PROC_DESC + .get_or_init(|| { + ::mudu_contract::procedure::proc_desc::ProcDesc::new( + "wallet".to_string(), + "transfer".to_string(), + mudu_argv_desc_transfer().clone(), + mudu_result_desc_transfer().clone(), + false + ) + }) } mod mod_transfer { @@ -1156,10 +1424,10 @@ mod mod_transfer { struct GuestTransfer {} impl Guest for GuestTransfer { - async fn mp2_transfer(param: Vec) -> Vec { + async fn mp2_transfer(param:Vec) -> Vec { super::mp2_transfer(param).await } } export!(GuestTransfer); -} +} \ No newline at end of file diff --git a/example/wallet/src/generated/transactions.rs b/example/wallet/src/generated/transactions.rs index 62c52ff..947cc0f 100644 --- a/example/wallet/src/generated/transactions.rs +++ b/example/wallet/src/generated/transactions.rs @@ -1,640 +1,690 @@ pub mod object { - use lazy_static::lazy_static; - use mudu::common::result::RS; - use mudu_contract::database::attr_field_access; - use mudu_contract::database::attr_value::AttrValue; - use mudu_contract::database::entity::Entity; - use mudu_contract::database::entity_utils; - use mudu_contract::database::sql_params::SQLParamMarker; - use mudu_contract::tuple::datum_desc::DatumDesc; - use mudu_contract::tuple::tuple_datum::TupleDatumMarker; - use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; - use mudu_type::dat_binary::DatBinary; - use mudu_type::dat_textual::DatTextual; - use mudu_type::dat_type::DatType; - use mudu_type::dat_type_id::DatTypeID; - use mudu_type::dat_value::DatValue; - use mudu_type::datum::{Datum, DatumDyn}; - - // constant definition - const TRANSACTIONS: &str = "transactions"; - - const TRANS_ID: &str = "trans_id"; - - const TRANS_TYPE: &str = "trans_type"; - - const FROM_USER: &str = "from_user"; - - const TO_USER: &str = "to_user"; - - const AMOUNT: &str = "amount"; - - const CREATED_AT: &str = "created_at"; - - // entity struct definition - #[derive(Debug, Clone, Default)] - pub struct Transactions { - trans_id: AttrTransId, - - trans_type: AttrTransType, - - from_user: AttrFromUser, - - to_user: AttrToUser, - - amount: AttrAmount, +use lazy_static::lazy_static; +use mudu::common::result::RS; +use mudu_type::dat_binary::DatBinary; +use mudu_type::dat_textual::DatTextual; +use mudu_type::dat_type::DatType; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dat_value::DatValue; +use mudu_type::datum::{Datum, DatumDyn}; +use mudu_contract::database::attr_field_access; +use mudu_contract::database::attr_value::AttrValue; +use mudu_contract::database::entity::Entity; +use mudu_contract::database::entity_utils; +use mudu_contract::tuple::datum_desc::DatumDesc; +use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; +use mudu_contract::tuple::tuple_datum::TupleDatumMarker; +use mudu_contract::database::sql_params::SQLParamMarker; + +// constant definition +const TRANSACTIONS:&str = "transactions"; + +const TRANS_ID:&str = "trans_id"; + +const TRANS_TYPE:&str = "trans_type"; + +const FROM_USER:&str = "from_user"; + +const TO_USER:&str = "to_user"; + +const AMOUNT:&str = "amount"; + +const CREATED_AT:&str = "created_at"; + + +// entity struct definition +#[derive(Debug, Clone, Default)] +pub struct Transactions { + + trans_id: AttrTransId, + + trans_type: AttrTransType, + + from_user: AttrFromUser, + + to_user: AttrToUser, + + amount: AttrAmount, + + created_at: AttrCreatedAt, + +} - created_at: AttrCreatedAt, +impl TupleDatumMarker for Transactions {} + +impl SQLParamMarker for Transactions {} + +impl Transactions { + pub fn new( + trans_id: Option, + trans_type: Option, + from_user: Option, + to_user: Option, + amount: Option, + created_at: Option, + + ) -> Self { + let s = Self { + + trans_id : AttrTransId::from(trans_id), + + trans_type : AttrTransType::from(trans_type), + + from_user : AttrFromUser::from(from_user), + + to_user : AttrToUser::from(to_user), + + amount : AttrAmount::from(amount), + + created_at : AttrCreatedAt::from(created_at), + + }; + s } - impl TupleDatumMarker for Transactions {} - - impl SQLParamMarker for Transactions {} + pub fn new_empty() -> Self { + Self::default() + } - impl Transactions { - pub fn new( - trans_id: Option, - trans_type: Option, - from_user: Option, - to_user: Option, - amount: Option, - created_at: Option, - ) -> Self { - let s = Self { - trans_id: AttrTransId::from(trans_id), + + pub fn set_trans_id( + &mut self, + trans_id: String, + ) { + self.trans_id.update(trans_id) + } - trans_type: AttrTransType::from(trans_type), + pub fn get_trans_id( + &self, + ) -> &Option { + self.trans_id.get() + } + + pub fn set_trans_type( + &mut self, + trans_type: String, + ) { + self.trans_type.update(trans_type) + } - from_user: AttrFromUser::from(from_user), + pub fn get_trans_type( + &self, + ) -> &Option { + self.trans_type.get() + } + + pub fn set_from_user( + &mut self, + from_user: i32, + ) { + self.from_user.update(from_user) + } - to_user: AttrToUser::from(to_user), + pub fn get_from_user( + &self, + ) -> &Option { + self.from_user.get() + } + + pub fn set_to_user( + &mut self, + to_user: i32, + ) { + self.to_user.update(to_user) + } - amount: AttrAmount::from(amount), + pub fn get_to_user( + &self, + ) -> &Option { + self.to_user.get() + } + + pub fn set_amount( + &mut self, + amount: i32, + ) { + self.amount.update(amount) + } - created_at: AttrCreatedAt::from(created_at), - }; - s - } + pub fn get_amount( + &self, + ) -> &Option { + self.amount.get() + } + + pub fn set_created_at( + &mut self, + created_at: i32, + ) { + self.created_at.update(created_at) + } - pub fn new_empty() -> Self { - Self::default() - } + pub fn get_created_at( + &self, + ) -> &Option { + self.created_at.get() + } + +} - pub fn set_trans_id(&mut self, trans_id: String) { - self.trans_id.update(trans_id) +impl Datum for Transactions { + fn dat_type() -> &'static DatType { + lazy_static! { + static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); } + &DAT_TYPE + } - pub fn get_trans_id(&self) -> &Option { - self.trans_id.get() - } + fn from_binary(binary: &[u8]) -> RS { + entity_utils::entity_from_binary(binary) + } - pub fn set_trans_type(&mut self, trans_type: String) { - self.trans_type.update(trans_type) - } + fn from_value(value: &DatValue) -> RS { + entity_utils::entity_from_value(value) + } - pub fn get_trans_type(&self) -> &Option { - self.trans_type.get() - } + fn from_textual(textual: &str) -> RS { + entity_utils::entity_from_textual(textual) + } +} - pub fn set_from_user(&mut self, from_user: i32) { - self.from_user.update(from_user) - } +impl DatumDyn for Transactions { + fn dat_type_id(&self) -> RS { + entity_utils::entity_dat_type_id() + } - pub fn get_from_user(&self) -> &Option { - self.from_user.get() - } + fn to_binary(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_binary(self, dat_type) + } - pub fn set_to_user(&mut self, to_user: i32) { - self.to_user.update(to_user) - } + fn to_textual(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_textual(self, dat_type) + } - pub fn get_to_user(&self) -> &Option { - self.to_user.get() - } + fn to_value(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_value(self, dat_type) + } - pub fn set_amount(&mut self, amount: i32) { - self.amount.update(amount) - } + fn clone_boxed(&self) -> Box { + entity_utils::entity_clone_boxed(self) + } +} - pub fn get_amount(&self) -> &Option { - self.amount.get() - } +impl Entity for Transactions { + fn new_empty() -> Self { + Self::new_empty() + } - pub fn set_created_at(&mut self, created_at: i32) { - self.created_at.update(created_at) - } + fn tuple_desc() -> &'static TupleFieldDesc { + lazy_static! { + static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ + + AttrTransId::datum_desc().clone(), + + AttrTransType::datum_desc().clone(), + + AttrFromUser::datum_desc().clone(), + + AttrToUser::datum_desc().clone(), + + AttrAmount::datum_desc().clone(), + + AttrCreatedAt::datum_desc().clone(), + + ]); + } + &TUPLE_DESC + } - pub fn get_created_at(&self) -> &Option { - self.created_at.get() - } + fn object_name() -> &'static str { + TRANSACTIONS } - impl Datum for Transactions { - fn dat_type() -> &'static DatType { - lazy_static! { - static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); + fn get_field_binary(&self, field: &str) -> RS>> { + match field { + + TRANS_ID => { + attr_field_access::attr_get_binary::<_>(self.trans_id.get()) } - &DAT_TYPE - } - - fn from_binary(binary: &[u8]) -> RS { - entity_utils::entity_from_binary(binary) - } - - fn from_value(value: &DatValue) -> RS { - entity_utils::entity_from_value(value) - } - - fn from_textual(textual: &str) -> RS { - entity_utils::entity_from_textual(textual) + + TRANS_TYPE => { + attr_field_access::attr_get_binary::<_>(self.trans_type.get()) + } + + FROM_USER => { + attr_field_access::attr_get_binary::<_>(self.from_user.get()) + } + + TO_USER => { + attr_field_access::attr_get_binary::<_>(self.to_user.get()) + } + + AMOUNT => { + attr_field_access::attr_get_binary::<_>(self.amount.get()) + } + + CREATED_AT => { + attr_field_access::attr_get_binary::<_>(self.created_at.get()) + } + + _ => { panic!("unknown name"); } } } - impl DatumDyn for Transactions { - fn dat_type_id(&self) -> RS { - entity_utils::entity_dat_type_id() - } - - fn to_binary(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_binary(self, dat_type) - } - - fn to_textual(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_textual(self, dat_type) - } - - fn to_value(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_value(self, dat_type) - } - - fn clone_boxed(&self) -> Box { - entity_utils::entity_clone_boxed(self) + fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { + match field { + + TRANS_ID => { + attr_field_access::attr_set_binary::<_, _>(self.trans_id.get_mut(), binary.as_ref())?; + } + + TRANS_TYPE => { + attr_field_access::attr_set_binary::<_, _>(self.trans_type.get_mut(), binary.as_ref())?; + } + + FROM_USER => { + attr_field_access::attr_set_binary::<_, _>(self.from_user.get_mut(), binary.as_ref())?; + } + + TO_USER => { + attr_field_access::attr_set_binary::<_, _>(self.to_user.get_mut(), binary.as_ref())?; + } + + AMOUNT => { + attr_field_access::attr_set_binary::<_, _>(self.amount.get_mut(), binary.as_ref())?; + } + + CREATED_AT => { + attr_field_access::attr_set_binary::<_, _>(self.created_at.get_mut(), binary.as_ref())?; + } + + _ => { panic!("unknown name"); } } + Ok(()) } - impl Entity for Transactions { - fn new_empty() -> Self { - Self::new_empty() - } - - fn tuple_desc() -> &'static TupleFieldDesc { - lazy_static! { - static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ - AttrTransId::datum_desc().clone(), - AttrTransType::datum_desc().clone(), - AttrFromUser::datum_desc().clone(), - AttrToUser::datum_desc().clone(), - AttrAmount::datum_desc().clone(), - AttrCreatedAt::datum_desc().clone(), - ]); + fn get_field_value(&self, field: &str) -> RS> { + match field { + + TRANS_ID => { + attr_field_access::attr_get_value::<_>(self.trans_id.get()) } - &TUPLE_DESC - } - - fn object_name() -> &'static str { - TRANSACTIONS - } - - fn get_field_binary(&self, field: &str) -> RS>> { - match field { - TRANS_ID => attr_field_access::attr_get_binary::<_>(self.trans_id.get()), - - TRANS_TYPE => attr_field_access::attr_get_binary::<_>(self.trans_type.get()), - - FROM_USER => attr_field_access::attr_get_binary::<_>(self.from_user.get()), - - TO_USER => attr_field_access::attr_get_binary::<_>(self.to_user.get()), - - AMOUNT => attr_field_access::attr_get_binary::<_>(self.amount.get()), - - CREATED_AT => attr_field_access::attr_get_binary::<_>(self.created_at.get()), - - _ => { - panic!("unknown name"); - } + + TRANS_TYPE => { + attr_field_access::attr_get_value::<_>(self.trans_type.get()) } - } - - fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { - match field { - TRANS_ID => { - attr_field_access::attr_set_binary::<_, _>( - self.trans_id.get_mut(), - binary.as_ref(), - )?; - } - - TRANS_TYPE => { - attr_field_access::attr_set_binary::<_, _>( - self.trans_type.get_mut(), - binary.as_ref(), - )?; - } - - FROM_USER => { - attr_field_access::attr_set_binary::<_, _>( - self.from_user.get_mut(), - binary.as_ref(), - )?; - } - - TO_USER => { - attr_field_access::attr_set_binary::<_, _>( - self.to_user.get_mut(), - binary.as_ref(), - )?; - } - - AMOUNT => { - attr_field_access::attr_set_binary::<_, _>( - self.amount.get_mut(), - binary.as_ref(), - )?; - } - - CREATED_AT => { - attr_field_access::attr_set_binary::<_, _>( - self.created_at.get_mut(), - binary.as_ref(), - )?; - } - - _ => { - panic!("unknown name"); - } + + FROM_USER => { + attr_field_access::attr_get_value::<_>(self.from_user.get()) + } + + TO_USER => { + attr_field_access::attr_get_value::<_>(self.to_user.get()) + } + + AMOUNT => { + attr_field_access::attr_get_value::<_>(self.amount.get()) + } + + CREATED_AT => { + attr_field_access::attr_get_value::<_>(self.created_at.get()) } - Ok(()) + + _ => { panic!("unknown name"); } } + } - fn get_field_value(&self, field: &str) -> RS> { - match field { - TRANS_ID => attr_field_access::attr_get_value::<_>(self.trans_id.get()), - - TRANS_TYPE => attr_field_access::attr_get_value::<_>(self.trans_type.get()), - - FROM_USER => attr_field_access::attr_get_value::<_>(self.from_user.get()), - - TO_USER => attr_field_access::attr_get_value::<_>(self.to_user.get()), - - AMOUNT => attr_field_access::attr_get_value::<_>(self.amount.get()), - - CREATED_AT => attr_field_access::attr_get_value::<_>(self.created_at.get()), - - _ => { - panic!("unknown name"); - } + fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { + match field { + + TRANS_ID => { + attr_field_access::attr_set_value::<_, _>(self.trans_id.get_mut(), value)?; + } + + TRANS_TYPE => { + attr_field_access::attr_set_value::<_, _>(self.trans_type.get_mut(), value)?; + } + + FROM_USER => { + attr_field_access::attr_set_value::<_, _>(self.from_user.get_mut(), value)?; + } + + TO_USER => { + attr_field_access::attr_set_value::<_, _>(self.to_user.get_mut(), value)?; } + + AMOUNT => { + attr_field_access::attr_set_value::<_, _>(self.amount.get_mut(), value)?; + } + + CREATED_AT => { + attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; + } + + _ => { panic!("unknown name"); } } + Ok(()) + } +} - fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { - match field { - TRANS_ID => { - attr_field_access::attr_set_value::<_, _>(self.trans_id.get_mut(), value)?; - } - - TRANS_TYPE => { - attr_field_access::attr_set_value::<_, _>(self.trans_type.get_mut(), value)?; - } - - FROM_USER => { - attr_field_access::attr_set_value::<_, _>(self.from_user.get_mut(), value)?; - } - - TO_USER => { - attr_field_access::attr_set_value::<_, _>(self.to_user.get_mut(), value)?; - } - - AMOUNT => { - attr_field_access::attr_set_value::<_, _>(self.amount.get_mut(), value)?; - } - CREATED_AT => { - attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrTransId { + is_dirty:bool, + value: Option +} - _ => { - panic!("unknown name"); - } - } - Ok(()) +impl AttrTransId { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrTransId { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrTransId { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrTransId { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrTransId { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + TRANS_ID + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrTransType { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - TRANS_ID +impl AttrTransType { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrTransType { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrTransType { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrTransType { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrTransType { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + TRANS_TYPE + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrFromUser { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - TRANS_TYPE +impl AttrFromUser { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrFromUser { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrFromUser { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrFromUser { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrFromUser { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + FROM_USER + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrToUser { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - FROM_USER +impl AttrToUser { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrToUser { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrToUser { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrToUser { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrToUser { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + TO_USER + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrAmount { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - TO_USER +impl AttrAmount { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrAmount { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrAmount { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrAmount { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrAmount { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + AMOUNT + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrCreatedAt { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - AMOUNT +impl AttrCreatedAt { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrCreatedAt { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrCreatedAt { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } - - fn get(&self) -> &Option { - &self.value - } - - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn set(&mut self, value: Option) { - self.value = value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) } +} - impl AttrValue for AttrCreatedAt { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } +impl AttrValue for AttrCreatedAt { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) + } - fn object_name() -> &'static str { - TRANSACTIONS - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn attr_name() -> &'static str { - CREATED_AT - } + fn attr_name() -> &'static str { + CREATED_AT } } + + +} \ No newline at end of file diff --git a/example/wallet/src/generated/users.rs b/example/wallet/src/generated/users.rs index a007e74..0f02ddc 100644 --- a/example/wallet/src/generated/users.rs +++ b/example/wallet/src/generated/users.rs @@ -1,724 +1,781 @@ pub mod object { - use lazy_static::lazy_static; - use mudu::common::result::RS; - use mudu_contract::database::attr_field_access; - use mudu_contract::database::attr_value::AttrValue; - use mudu_contract::database::entity::Entity; - use mudu_contract::database::entity_utils; - use mudu_contract::database::sql_params::SQLParamMarker; - use mudu_contract::tuple::datum_desc::DatumDesc; - use mudu_contract::tuple::tuple_datum::TupleDatumMarker; - use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; - use mudu_type::dat_binary::DatBinary; - use mudu_type::dat_textual::DatTextual; - use mudu_type::dat_type::DatType; - use mudu_type::dat_type_id::DatTypeID; - use mudu_type::dat_value::DatValue; - use mudu_type::datum::{Datum, DatumDyn}; - - // constant definition - const USERS: &str = "users"; - - const USER_ID: &str = "user_id"; - - const NAME: &str = "name"; - - const PHONE: &str = "phone"; - - const EMAIL: &str = "email"; - - const PASSWORD: &str = "password"; - - const CREATED_AT: &str = "created_at"; - - const UPDATED_AT: &str = "updated_at"; - - // entity struct definition - #[derive(Debug, Clone, Default)] - pub struct Users { - user_id: AttrUserId, - - name: AttrName, - - phone: AttrPhone, - - email: AttrEmail, - - password: AttrPassword, - - created_at: AttrCreatedAt, +use lazy_static::lazy_static; +use mudu::common::result::RS; +use mudu_type::dat_binary::DatBinary; +use mudu_type::dat_textual::DatTextual; +use mudu_type::dat_type::DatType; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dat_value::DatValue; +use mudu_type::datum::{Datum, DatumDyn}; +use mudu_contract::database::attr_field_access; +use mudu_contract::database::attr_value::AttrValue; +use mudu_contract::database::entity::Entity; +use mudu_contract::database::entity_utils; +use mudu_contract::tuple::datum_desc::DatumDesc; +use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; +use mudu_contract::tuple::tuple_datum::TupleDatumMarker; +use mudu_contract::database::sql_params::SQLParamMarker; + +// constant definition +const USERS:&str = "users"; + +const USER_ID:&str = "user_id"; + +const NAME:&str = "name"; + +const PHONE:&str = "phone"; + +const EMAIL:&str = "email"; + +const PASSWORD:&str = "password"; + +const CREATED_AT:&str = "created_at"; + +const UPDATED_AT:&str = "updated_at"; + + +// entity struct definition +#[derive(Debug, Clone, Default)] +pub struct Users { + + user_id: AttrUserId, + + name: AttrName, + + phone: AttrPhone, + + email: AttrEmail, + + password: AttrPassword, + + created_at: AttrCreatedAt, + + updated_at: AttrUpdatedAt, + +} - updated_at: AttrUpdatedAt, +impl TupleDatumMarker for Users {} + +impl SQLParamMarker for Users {} + +impl Users { + pub fn new( + user_id: Option, + name: Option, + phone: Option, + email: Option, + password: Option, + created_at: Option, + updated_at: Option, + + ) -> Self { + let s = Self { + + user_id : AttrUserId::from(user_id), + + name : AttrName::from(name), + + phone : AttrPhone::from(phone), + + email : AttrEmail::from(email), + + password : AttrPassword::from(password), + + created_at : AttrCreatedAt::from(created_at), + + updated_at : AttrUpdatedAt::from(updated_at), + + }; + s } - impl TupleDatumMarker for Users {} - - impl SQLParamMarker for Users {} - - impl Users { - pub fn new( - user_id: Option, - name: Option, - phone: Option, - email: Option, - password: Option, - created_at: Option, - updated_at: Option, - ) -> Self { - let s = Self { - user_id: AttrUserId::from(user_id), - - name: AttrName::from(name), + pub fn new_empty() -> Self { + Self::default() + } - phone: AttrPhone::from(phone), + + pub fn set_user_id( + &mut self, + user_id: i32, + ) { + self.user_id.update(user_id) + } - email: AttrEmail::from(email), + pub fn get_user_id( + &self, + ) -> &Option { + self.user_id.get() + } + + pub fn set_name( + &mut self, + name: String, + ) { + self.name.update(name) + } - password: AttrPassword::from(password), + pub fn get_name( + &self, + ) -> &Option { + self.name.get() + } + + pub fn set_phone( + &mut self, + phone: String, + ) { + self.phone.update(phone) + } - created_at: AttrCreatedAt::from(created_at), + pub fn get_phone( + &self, + ) -> &Option { + self.phone.get() + } + + pub fn set_email( + &mut self, + email: String, + ) { + self.email.update(email) + } - updated_at: AttrUpdatedAt::from(updated_at), - }; - s - } + pub fn get_email( + &self, + ) -> &Option { + self.email.get() + } + + pub fn set_password( + &mut self, + password: String, + ) { + self.password.update(password) + } - pub fn new_empty() -> Self { - Self::default() - } + pub fn get_password( + &self, + ) -> &Option { + self.password.get() + } + + pub fn set_created_at( + &mut self, + created_at: i32, + ) { + self.created_at.update(created_at) + } - pub fn set_user_id(&mut self, user_id: i32) { - self.user_id.update(user_id) - } + pub fn get_created_at( + &self, + ) -> &Option { + self.created_at.get() + } + + pub fn set_updated_at( + &mut self, + updated_at: i32, + ) { + self.updated_at.update(updated_at) + } - pub fn get_user_id(&self) -> &Option { - self.user_id.get() - } + pub fn get_updated_at( + &self, + ) -> &Option { + self.updated_at.get() + } + +} - pub fn set_name(&mut self, name: String) { - self.name.update(name) +impl Datum for Users { + fn dat_type() -> &'static DatType { + lazy_static! { + static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); } + &DAT_TYPE + } - pub fn get_name(&self) -> &Option { - self.name.get() - } + fn from_binary(binary: &[u8]) -> RS { + entity_utils::entity_from_binary(binary) + } - pub fn set_phone(&mut self, phone: String) { - self.phone.update(phone) - } + fn from_value(value: &DatValue) -> RS { + entity_utils::entity_from_value(value) + } - pub fn get_phone(&self) -> &Option { - self.phone.get() - } + fn from_textual(textual: &str) -> RS { + entity_utils::entity_from_textual(textual) + } +} - pub fn set_email(&mut self, email: String) { - self.email.update(email) - } +impl DatumDyn for Users { + fn dat_type_id(&self) -> RS { + entity_utils::entity_dat_type_id() + } - pub fn get_email(&self) -> &Option { - self.email.get() - } + fn to_binary(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_binary(self, dat_type) + } - pub fn set_password(&mut self, password: String) { - self.password.update(password) - } + fn to_textual(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_textual(self, dat_type) + } - pub fn get_password(&self) -> &Option { - self.password.get() - } + fn to_value(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_value(self, dat_type) + } - pub fn set_created_at(&mut self, created_at: i32) { - self.created_at.update(created_at) - } + fn clone_boxed(&self) -> Box { + entity_utils::entity_clone_boxed(self) + } +} - pub fn get_created_at(&self) -> &Option { - self.created_at.get() - } +impl Entity for Users { + fn new_empty() -> Self { + Self::new_empty() + } - pub fn set_updated_at(&mut self, updated_at: i32) { - self.updated_at.update(updated_at) - } + fn tuple_desc() -> &'static TupleFieldDesc { + lazy_static! { + static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ + + AttrUserId::datum_desc().clone(), + + AttrName::datum_desc().clone(), + + AttrPhone::datum_desc().clone(), + + AttrEmail::datum_desc().clone(), + + AttrPassword::datum_desc().clone(), + + AttrCreatedAt::datum_desc().clone(), + + AttrUpdatedAt::datum_desc().clone(), + + ]); + } + &TUPLE_DESC + } - pub fn get_updated_at(&self) -> &Option { - self.updated_at.get() - } + fn object_name() -> &'static str { + USERS } - impl Datum for Users { - fn dat_type() -> &'static DatType { - lazy_static! { - static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); + fn get_field_binary(&self, field: &str) -> RS>> { + match field { + + USER_ID => { + attr_field_access::attr_get_binary::<_>(self.user_id.get()) } - &DAT_TYPE - } - - fn from_binary(binary: &[u8]) -> RS { - entity_utils::entity_from_binary(binary) - } - - fn from_value(value: &DatValue) -> RS { - entity_utils::entity_from_value(value) - } - - fn from_textual(textual: &str) -> RS { - entity_utils::entity_from_textual(textual) + + NAME => { + attr_field_access::attr_get_binary::<_>(self.name.get()) + } + + PHONE => { + attr_field_access::attr_get_binary::<_>(self.phone.get()) + } + + EMAIL => { + attr_field_access::attr_get_binary::<_>(self.email.get()) + } + + PASSWORD => { + attr_field_access::attr_get_binary::<_>(self.password.get()) + } + + CREATED_AT => { + attr_field_access::attr_get_binary::<_>(self.created_at.get()) + } + + UPDATED_AT => { + attr_field_access::attr_get_binary::<_>(self.updated_at.get()) + } + + _ => { panic!("unknown name"); } } } - impl DatumDyn for Users { - fn dat_type_id(&self) -> RS { - entity_utils::entity_dat_type_id() - } - - fn to_binary(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_binary(self, dat_type) - } - - fn to_textual(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_textual(self, dat_type) - } - - fn to_value(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_value(self, dat_type) - } - - fn clone_boxed(&self) -> Box { - entity_utils::entity_clone_boxed(self) + fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { + match field { + + USER_ID => { + attr_field_access::attr_set_binary::<_, _>(self.user_id.get_mut(), binary.as_ref())?; + } + + NAME => { + attr_field_access::attr_set_binary::<_, _>(self.name.get_mut(), binary.as_ref())?; + } + + PHONE => { + attr_field_access::attr_set_binary::<_, _>(self.phone.get_mut(), binary.as_ref())?; + } + + EMAIL => { + attr_field_access::attr_set_binary::<_, _>(self.email.get_mut(), binary.as_ref())?; + } + + PASSWORD => { + attr_field_access::attr_set_binary::<_, _>(self.password.get_mut(), binary.as_ref())?; + } + + CREATED_AT => { + attr_field_access::attr_set_binary::<_, _>(self.created_at.get_mut(), binary.as_ref())?; + } + + UPDATED_AT => { + attr_field_access::attr_set_binary::<_, _>(self.updated_at.get_mut(), binary.as_ref())?; + } + + _ => { panic!("unknown name"); } } + Ok(()) } - impl Entity for Users { - fn new_empty() -> Self { - Self::new_empty() - } - - fn tuple_desc() -> &'static TupleFieldDesc { - lazy_static! { - static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ - AttrUserId::datum_desc().clone(), - AttrName::datum_desc().clone(), - AttrPhone::datum_desc().clone(), - AttrEmail::datum_desc().clone(), - AttrPassword::datum_desc().clone(), - AttrCreatedAt::datum_desc().clone(), - AttrUpdatedAt::datum_desc().clone(), - ]); + fn get_field_value(&self, field: &str) -> RS> { + match field { + + USER_ID => { + attr_field_access::attr_get_value::<_>(self.user_id.get()) } - &TUPLE_DESC - } - - fn object_name() -> &'static str { - USERS - } - - fn get_field_binary(&self, field: &str) -> RS>> { - match field { - USER_ID => attr_field_access::attr_get_binary::<_>(self.user_id.get()), - - NAME => attr_field_access::attr_get_binary::<_>(self.name.get()), - - PHONE => attr_field_access::attr_get_binary::<_>(self.phone.get()), - - EMAIL => attr_field_access::attr_get_binary::<_>(self.email.get()), - - PASSWORD => attr_field_access::attr_get_binary::<_>(self.password.get()), - - CREATED_AT => attr_field_access::attr_get_binary::<_>(self.created_at.get()), - - UPDATED_AT => attr_field_access::attr_get_binary::<_>(self.updated_at.get()), - - _ => { - panic!("unknown name"); - } + + NAME => { + attr_field_access::attr_get_value::<_>(self.name.get()) } - } - - fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { - match field { - USER_ID => { - attr_field_access::attr_set_binary::<_, _>( - self.user_id.get_mut(), - binary.as_ref(), - )?; - } - - NAME => { - attr_field_access::attr_set_binary::<_, _>( - self.name.get_mut(), - binary.as_ref(), - )?; - } - - PHONE => { - attr_field_access::attr_set_binary::<_, _>( - self.phone.get_mut(), - binary.as_ref(), - )?; - } - - EMAIL => { - attr_field_access::attr_set_binary::<_, _>( - self.email.get_mut(), - binary.as_ref(), - )?; - } - - PASSWORD => { - attr_field_access::attr_set_binary::<_, _>( - self.password.get_mut(), - binary.as_ref(), - )?; - } - - CREATED_AT => { - attr_field_access::attr_set_binary::<_, _>( - self.created_at.get_mut(), - binary.as_ref(), - )?; - } - - UPDATED_AT => { - attr_field_access::attr_set_binary::<_, _>( - self.updated_at.get_mut(), - binary.as_ref(), - )?; - } - - _ => { - panic!("unknown name"); - } + + PHONE => { + attr_field_access::attr_get_value::<_>(self.phone.get()) + } + + EMAIL => { + attr_field_access::attr_get_value::<_>(self.email.get()) + } + + PASSWORD => { + attr_field_access::attr_get_value::<_>(self.password.get()) } - Ok(()) + + CREATED_AT => { + attr_field_access::attr_get_value::<_>(self.created_at.get()) + } + + UPDATED_AT => { + attr_field_access::attr_get_value::<_>(self.updated_at.get()) + } + + _ => { panic!("unknown name"); } } + } - fn get_field_value(&self, field: &str) -> RS> { - match field { - USER_ID => attr_field_access::attr_get_value::<_>(self.user_id.get()), - - NAME => attr_field_access::attr_get_value::<_>(self.name.get()), - - PHONE => attr_field_access::attr_get_value::<_>(self.phone.get()), - - EMAIL => attr_field_access::attr_get_value::<_>(self.email.get()), - - PASSWORD => attr_field_access::attr_get_value::<_>(self.password.get()), - - CREATED_AT => attr_field_access::attr_get_value::<_>(self.created_at.get()), - - UPDATED_AT => attr_field_access::attr_get_value::<_>(self.updated_at.get()), - - _ => { - panic!("unknown name"); - } + fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { + match field { + + USER_ID => { + attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; + } + + NAME => { + attr_field_access::attr_set_value::<_, _>(self.name.get_mut(), value)?; + } + + PHONE => { + attr_field_access::attr_set_value::<_, _>(self.phone.get_mut(), value)?; + } + + EMAIL => { + attr_field_access::attr_set_value::<_, _>(self.email.get_mut(), value)?; + } + + PASSWORD => { + attr_field_access::attr_set_value::<_, _>(self.password.get_mut(), value)?; + } + + CREATED_AT => { + attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; + } + + UPDATED_AT => { + attr_field_access::attr_set_value::<_, _>(self.updated_at.get_mut(), value)?; } + + _ => { panic!("unknown name"); } } + Ok(()) + } +} - fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { - match field { - USER_ID => { - attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; - } - - NAME => { - attr_field_access::attr_set_value::<_, _>(self.name.get_mut(), value)?; - } - - PHONE => { - attr_field_access::attr_set_value::<_, _>(self.phone.get_mut(), value)?; - } - - EMAIL => { - attr_field_access::attr_set_value::<_, _>(self.email.get_mut(), value)?; - } - - PASSWORD => { - attr_field_access::attr_set_value::<_, _>(self.password.get_mut(), value)?; - } - - CREATED_AT => { - attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; - } - UPDATED_AT => { - attr_field_access::attr_set_value::<_, _>(self.updated_at.get_mut(), value)?; - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrUserId { + is_dirty:bool, + value: Option +} - _ => { - panic!("unknown name"); - } - } - Ok(()) +impl AttrUserId { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrUserId { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrUserId { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrUserId { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrUserId { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + USER_ID + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrName { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - USER_ID +impl AttrName { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrName { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrName { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrName { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrName { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + NAME + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrPhone { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - NAME +impl AttrPhone { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrPhone { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrPhone { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrPhone { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrPhone { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + PHONE + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrEmail { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - PHONE +impl AttrEmail { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrEmail { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrEmail { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrEmail { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrEmail { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + EMAIL + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrPassword { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - EMAIL +impl AttrPassword { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrPassword { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrPassword { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrPassword { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrPassword { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + PASSWORD + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrCreatedAt { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - PASSWORD +impl AttrCreatedAt { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrCreatedAt { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrCreatedAt { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrCreatedAt { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrCreatedAt { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + CREATED_AT + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrUpdatedAt { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - CREATED_AT +impl AttrUpdatedAt { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrUpdatedAt { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrUpdatedAt { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } - - fn get(&self) -> &Option { - &self.value - } - - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn set(&mut self, value: Option) { - self.value = value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) } +} - impl AttrValue for AttrUpdatedAt { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } +impl AttrValue for AttrUpdatedAt { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) + } - fn object_name() -> &'static str { - USERS - } + fn object_name() -> &'static str { + USERS + } - fn attr_name() -> &'static str { - UPDATED_AT - } + fn attr_name() -> &'static str { + UPDATED_AT } } + + +} \ No newline at end of file diff --git a/example/wallet/src/generated/wallets.rs b/example/wallet/src/generated/wallets.rs index 420d8d6..b0aa482 100644 --- a/example/wallet/src/generated/wallets.rs +++ b/example/wallet/src/generated/wallets.rs @@ -1,384 +1,417 @@ pub mod object { - use lazy_static::lazy_static; - use mudu::common::result::RS; - use mudu_contract::database::attr_field_access; - use mudu_contract::database::attr_value::AttrValue; - use mudu_contract::database::entity::Entity; - use mudu_contract::database::entity_utils; - use mudu_contract::database::sql_params::SQLParamMarker; - use mudu_contract::tuple::datum_desc::DatumDesc; - use mudu_contract::tuple::tuple_datum::TupleDatumMarker; - use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; - use mudu_type::dat_binary::DatBinary; - use mudu_type::dat_textual::DatTextual; - use mudu_type::dat_type::DatType; - use mudu_type::dat_type_id::DatTypeID; - use mudu_type::dat_value::DatValue; - use mudu_type::datum::{Datum, DatumDyn}; - - // constant definition - const WALLETS: &str = "wallets"; - - const USER_ID: &str = "user_id"; - - const BALANCE: &str = "balance"; - - const UPDATED_AT: &str = "updated_at"; - - // entity struct definition - #[derive(Debug, Clone, Default)] - pub struct Wallets { - user_id: AttrUserId, - - balance: AttrBalance, +use lazy_static::lazy_static; +use mudu::common::result::RS; +use mudu_type::dat_binary::DatBinary; +use mudu_type::dat_textual::DatTextual; +use mudu_type::dat_type::DatType; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dat_value::DatValue; +use mudu_type::datum::{Datum, DatumDyn}; +use mudu_contract::database::attr_field_access; +use mudu_contract::database::attr_value::AttrValue; +use mudu_contract::database::entity::Entity; +use mudu_contract::database::entity_utils; +use mudu_contract::tuple::datum_desc::DatumDesc; +use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; +use mudu_contract::tuple::tuple_datum::TupleDatumMarker; +use mudu_contract::database::sql_params::SQLParamMarker; + +// constant definition +const WALLETS:&str = "wallets"; + +const USER_ID:&str = "user_id"; + +const BALANCE:&str = "balance"; + +const UPDATED_AT:&str = "updated_at"; + + +// entity struct definition +#[derive(Debug, Clone, Default)] +pub struct Wallets { + + user_id: AttrUserId, + + balance: AttrBalance, + + updated_at: AttrUpdatedAt, + +} - updated_at: AttrUpdatedAt, +impl TupleDatumMarker for Wallets {} + +impl SQLParamMarker for Wallets {} + +impl Wallets { + pub fn new( + user_id: Option, + balance: Option, + updated_at: Option, + + ) -> Self { + let s = Self { + + user_id : AttrUserId::from(user_id), + + balance : AttrBalance::from(balance), + + updated_at : AttrUpdatedAt::from(updated_at), + + }; + s } - impl TupleDatumMarker for Wallets {} - - impl SQLParamMarker for Wallets {} - - impl Wallets { - pub fn new(user_id: Option, balance: Option, updated_at: Option) -> Self { - let s = Self { - user_id: AttrUserId::from(user_id), - - balance: AttrBalance::from(balance), - - updated_at: AttrUpdatedAt::from(updated_at), - }; - s - } - - pub fn new_empty() -> Self { - Self::default() - } + pub fn new_empty() -> Self { + Self::default() + } - pub fn set_user_id(&mut self, user_id: i32) { - self.user_id.update(user_id) - } + + pub fn set_user_id( + &mut self, + user_id: i32, + ) { + self.user_id.update(user_id) + } - pub fn get_user_id(&self) -> &Option { - self.user_id.get() - } + pub fn get_user_id( + &self, + ) -> &Option { + self.user_id.get() + } + + pub fn set_balance( + &mut self, + balance: i32, + ) { + self.balance.update(balance) + } - pub fn set_balance(&mut self, balance: i32) { - self.balance.update(balance) - } + pub fn get_balance( + &self, + ) -> &Option { + self.balance.get() + } + + pub fn set_updated_at( + &mut self, + updated_at: i32, + ) { + self.updated_at.update(updated_at) + } - pub fn get_balance(&self) -> &Option { - self.balance.get() - } + pub fn get_updated_at( + &self, + ) -> &Option { + self.updated_at.get() + } + +} - pub fn set_updated_at(&mut self, updated_at: i32) { - self.updated_at.update(updated_at) +impl Datum for Wallets { + fn dat_type() -> &'static DatType { + lazy_static! { + static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); } + &DAT_TYPE + } - pub fn get_updated_at(&self) -> &Option { - self.updated_at.get() - } + fn from_binary(binary: &[u8]) -> RS { + entity_utils::entity_from_binary(binary) } - impl Datum for Wallets { - fn dat_type() -> &'static DatType { - lazy_static! { - static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); - } - &DAT_TYPE - } + fn from_value(value: &DatValue) -> RS { + entity_utils::entity_from_value(value) + } - fn from_binary(binary: &[u8]) -> RS { - entity_utils::entity_from_binary(binary) - } + fn from_textual(textual: &str) -> RS { + entity_utils::entity_from_textual(textual) + } +} - fn from_value(value: &DatValue) -> RS { - entity_utils::entity_from_value(value) - } +impl DatumDyn for Wallets { + fn dat_type_id(&self) -> RS { + entity_utils::entity_dat_type_id() + } - fn from_textual(textual: &str) -> RS { - entity_utils::entity_from_textual(textual) - } + fn to_binary(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_binary(self, dat_type) } - impl DatumDyn for Wallets { - fn dat_type_id(&self) -> RS { - entity_utils::entity_dat_type_id() - } + fn to_textual(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_textual(self, dat_type) + } - fn to_binary(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_binary(self, dat_type) - } + fn to_value(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_value(self, dat_type) + } - fn to_textual(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_textual(self, dat_type) - } + fn clone_boxed(&self) -> Box { + entity_utils::entity_clone_boxed(self) + } +} - fn to_value(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_value(self, dat_type) - } +impl Entity for Wallets { + fn new_empty() -> Self { + Self::new_empty() + } - fn clone_boxed(&self) -> Box { - entity_utils::entity_clone_boxed(self) - } + fn tuple_desc() -> &'static TupleFieldDesc { + lazy_static! { + static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ + + AttrUserId::datum_desc().clone(), + + AttrBalance::datum_desc().clone(), + + AttrUpdatedAt::datum_desc().clone(), + + ]); + } + &TUPLE_DESC } - impl Entity for Wallets { - fn new_empty() -> Self { - Self::new_empty() - } + fn object_name() -> &'static str { + WALLETS + } - fn tuple_desc() -> &'static TupleFieldDesc { - lazy_static! { - static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ - AttrUserId::datum_desc().clone(), - AttrBalance::datum_desc().clone(), - AttrUpdatedAt::datum_desc().clone(), - ]); + fn get_field_binary(&self, field: &str) -> RS>> { + match field { + + USER_ID => { + attr_field_access::attr_get_binary::<_>(self.user_id.get()) } - &TUPLE_DESC - } - - fn object_name() -> &'static str { - WALLETS + + BALANCE => { + attr_field_access::attr_get_binary::<_>(self.balance.get()) + } + + UPDATED_AT => { + attr_field_access::attr_get_binary::<_>(self.updated_at.get()) + } + + _ => { panic!("unknown name"); } } + } - fn get_field_binary(&self, field: &str) -> RS>> { - match field { - USER_ID => attr_field_access::attr_get_binary::<_>(self.user_id.get()), - - BALANCE => attr_field_access::attr_get_binary::<_>(self.balance.get()), - - UPDATED_AT => attr_field_access::attr_get_binary::<_>(self.updated_at.get()), - - _ => { - panic!("unknown name"); - } + fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { + match field { + + USER_ID => { + attr_field_access::attr_set_binary::<_, _>(self.user_id.get_mut(), binary.as_ref())?; } + + BALANCE => { + attr_field_access::attr_set_binary::<_, _>(self.balance.get_mut(), binary.as_ref())?; + } + + UPDATED_AT => { + attr_field_access::attr_set_binary::<_, _>(self.updated_at.get_mut(), binary.as_ref())?; + } + + _ => { panic!("unknown name"); } } + Ok(()) + } - fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { - match field { - USER_ID => { - attr_field_access::attr_set_binary::<_, _>( - self.user_id.get_mut(), - binary.as_ref(), - )?; - } - - BALANCE => { - attr_field_access::attr_set_binary::<_, _>( - self.balance.get_mut(), - binary.as_ref(), - )?; - } - - UPDATED_AT => { - attr_field_access::attr_set_binary::<_, _>( - self.updated_at.get_mut(), - binary.as_ref(), - )?; - } - - _ => { - panic!("unknown name"); - } + fn get_field_value(&self, field: &str) -> RS> { + match field { + + USER_ID => { + attr_field_access::attr_get_value::<_>(self.user_id.get()) + } + + BALANCE => { + attr_field_access::attr_get_value::<_>(self.balance.get()) } - Ok(()) + + UPDATED_AT => { + attr_field_access::attr_get_value::<_>(self.updated_at.get()) + } + + _ => { panic!("unknown name"); } } + } - fn get_field_value(&self, field: &str) -> RS> { - match field { - USER_ID => attr_field_access::attr_get_value::<_>(self.user_id.get()), - - BALANCE => attr_field_access::attr_get_value::<_>(self.balance.get()), - - UPDATED_AT => attr_field_access::attr_get_value::<_>(self.updated_at.get()), - - _ => { - panic!("unknown name"); - } + fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { + match field { + + USER_ID => { + attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; + } + + BALANCE => { + attr_field_access::attr_set_value::<_, _>(self.balance.get_mut(), value)?; } + + UPDATED_AT => { + attr_field_access::attr_set_value::<_, _>(self.updated_at.get_mut(), value)?; + } + + _ => { panic!("unknown name"); } } + Ok(()) + } +} - fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { - match field { - USER_ID => { - attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; - } - - BALANCE => { - attr_field_access::attr_set_value::<_, _>(self.balance.get_mut(), value)?; - } - UPDATED_AT => { - attr_field_access::attr_set_value::<_, _>(self.updated_at.get_mut(), value)?; - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrUserId { + is_dirty:bool, + value: Option +} - _ => { - panic!("unknown name"); - } - } - Ok(()) +impl AttrUserId { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrUserId { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrUserId { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrUserId { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrUserId { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + WALLETS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + USER_ID + } +} - fn object_name() -> &'static str { - WALLETS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrBalance { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - USER_ID +impl AttrBalance { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrBalance { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrBalance { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrBalance { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrBalance { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + WALLETS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + BALANCE + } +} - fn object_name() -> &'static str { - WALLETS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrUpdatedAt { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - BALANCE +impl AttrUpdatedAt { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrUpdatedAt { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrUpdatedAt { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } - - fn get(&self) -> &Option { - &self.value - } - - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn set(&mut self, value: Option) { - self.value = value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) } +} - impl AttrValue for AttrUpdatedAt { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } +impl AttrValue for AttrUpdatedAt { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) + } - fn object_name() -> &'static str { - WALLETS - } + fn object_name() -> &'static str { + WALLETS + } - fn attr_name() -> &'static str { - UPDATED_AT - } + fn attr_name() -> &'static str { + UPDATED_AT } } + + +} \ No newline at end of file diff --git a/example/wallet/src/generated/warehouse.rs b/example/wallet/src/generated/warehouse.rs index 9544dd2..2403b76 100644 --- a/example/wallet/src/generated/warehouse.rs +++ b/example/wallet/src/generated/warehouse.rs @@ -158,7 +158,7 @@ pub mod object { } } - impl DatumDyn for Warehouse { +impl DatumDyn for Warehouse { fn dat_type_id(&self) -> RS { entity_utils::entity_dat_type_id() } @@ -175,10 +175,44 @@ pub mod object { entity_utils::entity_to_value(self, dat_type) } - fn clone_boxed(&self) -> Box { - entity_utils::entity_clone_boxed(self) - } + fn clone_boxed(&self) -> Box { + entity_utils::entity_clone_boxed(self) + } +} + +#[cfg(test)] +mod tests { + use super::Warehouse; + use mudu_type::datum::{Datum, DatumDyn}; + + #[test] + fn warehouse_roundtrip_and_setters_work() { + let mut warehouse = Warehouse::new( + Some(1), + Some(10.5), + Some(0.1), + Some("Main".to_string()), + Some("Street1".to_string()), + Some("Street2".to_string()), + Some("City".to_string()), + Some("ST".to_string()), + Some("10000".to_string()), + ); + + warehouse.set_w_name("Central".to_string()); + warehouse.set_w_tax(0.2); + assert_eq!(warehouse.get_w_name().as_deref(), Some("Central")); + assert_eq!(warehouse.get_w_tax(), &Some(0.2)); + + let from_value = Warehouse::from_value(&warehouse.to_value(Warehouse::dat_type()).unwrap()).unwrap(); + assert_eq!(from_value.get_w_city().as_deref(), Some("City")); + + let from_binary = + Warehouse::from_binary(warehouse.to_binary(Warehouse::dat_type()).unwrap().as_ref()) + .unwrap(); + assert_eq!(from_binary.get_w_zip().as_deref(), Some("10000")); } +} impl Entity for Warehouse { fn new_empty() -> Self { @@ -525,4 +559,4 @@ pub mod object { COLUMN_W_ZIP } } -} // end mod object +} // end mod object \ No newline at end of file diff --git a/example/wallet/src/rust/orders.rs b/example/wallet/src/rust/orders.rs index 26c49e9..51c38e4 100644 --- a/example/wallet/src/rust/orders.rs +++ b/example/wallet/src/rust/orders.rs @@ -1,556 +1,599 @@ pub mod object { - use lazy_static::lazy_static; - use mudu::common::result::RS; - use mudu_contract::database::attr_field_access; - use mudu_contract::database::attr_value::AttrValue; - use mudu_contract::database::entity::Entity; - use mudu_contract::database::entity_utils; - use mudu_contract::database::sql_params::SQLParamMarker; - use mudu_contract::tuple::datum_desc::DatumDesc; - use mudu_contract::tuple::tuple_datum::TupleDatumMarker; - use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; - use mudu_type::dat_binary::DatBinary; - use mudu_type::dat_textual::DatTextual; - use mudu_type::dat_type::DatType; - use mudu_type::dat_type_id::DatTypeID; - use mudu_type::dat_value::DatValue; - use mudu_type::datum::{Datum, DatumDyn}; - - // constant definition - const ORDERS: &str = "orders"; - - const ORDER_ID: &str = "order_id"; - - const USER_ID: &str = "user_id"; - - const MERCH_ID: &str = "merch_id"; - - const AMOUNT: &str = "amount"; - - const CREATED_AT: &str = "created_at"; - - // entity struct definition - #[derive(Debug, Clone, Default)] - pub struct Orders { - order_id: AttrOrderId, - - user_id: AttrUserId, - - merch_id: AttrMerchId, +use lazy_static::lazy_static; +use mudu::common::result::RS; +use mudu_type::dat_binary::DatBinary; +use mudu_type::dat_textual::DatTextual; +use mudu_type::dat_type::DatType; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dat_value::DatValue; +use mudu_type::datum::{Datum, DatumDyn}; +use mudu_contract::database::attr_field_access; +use mudu_contract::database::attr_value::AttrValue; +use mudu_contract::database::entity::Entity; +use mudu_contract::database::entity_utils; +use mudu_contract::tuple::datum_desc::DatumDesc; +use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; +use mudu_contract::tuple::tuple_datum::TupleDatumMarker; +use mudu_contract::database::sql_params::SQLParamMarker; + +// constant definition +const ORDERS:&str = "orders"; + +const ORDER_ID:&str = "order_id"; + +const USER_ID:&str = "user_id"; + +const MERCH_ID:&str = "merch_id"; + +const AMOUNT:&str = "amount"; + +const CREATED_AT:&str = "created_at"; + + +// entity struct definition +#[derive(Debug, Clone, Default)] +pub struct Orders { + + order_id: AttrOrderId, + + user_id: AttrUserId, + + merch_id: AttrMerchId, + + amount: AttrAmount, + + created_at: AttrCreatedAt, + +} - amount: AttrAmount, +impl TupleDatumMarker for Orders {} + +impl SQLParamMarker for Orders {} + +impl Orders { + pub fn new( + order_id: Option, + user_id: Option, + merch_id: Option, + amount: Option, + created_at: Option, + + ) -> Self { + let s = Self { + + order_id : AttrOrderId::from(order_id), + + user_id : AttrUserId::from(user_id), + + merch_id : AttrMerchId::from(merch_id), + + amount : AttrAmount::from(amount), + + created_at : AttrCreatedAt::from(created_at), + + }; + s + } - created_at: AttrCreatedAt, + pub fn new_empty() -> Self { + Self::default() } - impl TupleDatumMarker for Orders {} + + pub fn set_order_id( + &mut self, + order_id: i32, + ) { + self.order_id.update(order_id) + } - impl SQLParamMarker for Orders {} + pub fn get_order_id( + &self, + ) -> &Option { + self.order_id.get() + } + + pub fn set_user_id( + &mut self, + user_id: i32, + ) { + self.user_id.update(user_id) + } - impl Orders { - pub fn new( - order_id: Option, - user_id: Option, - merch_id: Option, - amount: Option, - created_at: Option, - ) -> Self { - let s = Self { - order_id: AttrOrderId::from(order_id), + pub fn get_user_id( + &self, + ) -> &Option { + self.user_id.get() + } + + pub fn set_merch_id( + &mut self, + merch_id: i32, + ) { + self.merch_id.update(merch_id) + } - user_id: AttrUserId::from(user_id), + pub fn get_merch_id( + &self, + ) -> &Option { + self.merch_id.get() + } + + pub fn set_amount( + &mut self, + amount: i32, + ) { + self.amount.update(amount) + } - merch_id: AttrMerchId::from(merch_id), + pub fn get_amount( + &self, + ) -> &Option { + self.amount.get() + } + + pub fn set_created_at( + &mut self, + created_at: i32, + ) { + self.created_at.update(created_at) + } - amount: AttrAmount::from(amount), + pub fn get_created_at( + &self, + ) -> &Option { + self.created_at.get() + } + +} - created_at: AttrCreatedAt::from(created_at), - }; - s +impl Datum for Orders { + fn dat_type() -> &'static DatType { + lazy_static! { + static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); } + &DAT_TYPE + } - pub fn new_empty() -> Self { - Self::default() - } + fn from_binary(binary: &[u8]) -> RS { + entity_utils::entity_from_binary(binary) + } - pub fn set_order_id(&mut self, order_id: i32) { - self.order_id.update(order_id) - } + fn from_value(value: &DatValue) -> RS { + entity_utils::entity_from_value(value) + } - pub fn get_order_id(&self) -> &Option { - self.order_id.get() - } + fn from_textual(textual: &str) -> RS { + entity_utils::entity_from_textual(textual) + } +} - pub fn set_user_id(&mut self, user_id: i32) { - self.user_id.update(user_id) - } +impl DatumDyn for Orders { + fn dat_type_id(&self) -> RS { + entity_utils::entity_dat_type_id() + } - pub fn get_user_id(&self) -> &Option { - self.user_id.get() - } + fn to_binary(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_binary(self, dat_type) + } - pub fn set_merch_id(&mut self, merch_id: i32) { - self.merch_id.update(merch_id) - } + fn to_textual(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_textual(self, dat_type) + } - pub fn get_merch_id(&self) -> &Option { - self.merch_id.get() - } + fn to_value(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_value(self, dat_type) + } - pub fn set_amount(&mut self, amount: i32) { - self.amount.update(amount) - } + fn clone_boxed(&self) -> Box { + entity_utils::entity_clone_boxed(self) + } +} - pub fn get_amount(&self) -> &Option { - self.amount.get() - } +impl Entity for Orders { + fn new_empty() -> Self { + Self::new_empty() + } - pub fn set_created_at(&mut self, created_at: i32) { - self.created_at.update(created_at) - } + fn tuple_desc() -> &'static TupleFieldDesc { + lazy_static! { + static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ + + AttrOrderId::datum_desc().clone(), + + AttrUserId::datum_desc().clone(), + + AttrMerchId::datum_desc().clone(), + + AttrAmount::datum_desc().clone(), + + AttrCreatedAt::datum_desc().clone(), + + ]); + } + &TUPLE_DESC + } - pub fn get_created_at(&self) -> &Option { - self.created_at.get() - } + fn object_name() -> &'static str { + ORDERS } - impl Datum for Orders { - fn dat_type() -> &'static DatType { - lazy_static! { - static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); + fn get_field_binary(&self, field: &str) -> RS>> { + match field { + + ORDER_ID => { + attr_field_access::attr_get_binary::<_>(self.order_id.get()) } - &DAT_TYPE - } - - fn from_binary(binary: &[u8]) -> RS { - entity_utils::entity_from_binary(binary) - } - - fn from_value(value: &DatValue) -> RS { - entity_utils::entity_from_value(value) - } - - fn from_textual(textual: &str) -> RS { - entity_utils::entity_from_textual(textual) + + USER_ID => { + attr_field_access::attr_get_binary::<_>(self.user_id.get()) + } + + MERCH_ID => { + attr_field_access::attr_get_binary::<_>(self.merch_id.get()) + } + + AMOUNT => { + attr_field_access::attr_get_binary::<_>(self.amount.get()) + } + + CREATED_AT => { + attr_field_access::attr_get_binary::<_>(self.created_at.get()) + } + + _ => { panic!("unknown name"); } } } - impl DatumDyn for Orders { - fn dat_type_id(&self) -> RS { - entity_utils::entity_dat_type_id() - } - - fn to_binary(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_binary(self, dat_type) - } - - fn to_textual(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_textual(self, dat_type) - } - - fn to_value(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_value(self, dat_type) - } - - fn clone_boxed(&self) -> Box { - entity_utils::entity_clone_boxed(self) + fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { + match field { + + ORDER_ID => { + attr_field_access::attr_set_binary::<_, _>(self.order_id.get_mut(), binary.as_ref())?; + } + + USER_ID => { + attr_field_access::attr_set_binary::<_, _>(self.user_id.get_mut(), binary.as_ref())?; + } + + MERCH_ID => { + attr_field_access::attr_set_binary::<_, _>(self.merch_id.get_mut(), binary.as_ref())?; + } + + AMOUNT => { + attr_field_access::attr_set_binary::<_, _>(self.amount.get_mut(), binary.as_ref())?; + } + + CREATED_AT => { + attr_field_access::attr_set_binary::<_, _>(self.created_at.get_mut(), binary.as_ref())?; + } + + _ => { panic!("unknown name"); } } + Ok(()) } - impl Entity for Orders { - fn new_empty() -> Self { - Self::new_empty() - } - - fn tuple_desc() -> &'static TupleFieldDesc { - lazy_static! { - static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ - AttrOrderId::datum_desc().clone(), - AttrUserId::datum_desc().clone(), - AttrMerchId::datum_desc().clone(), - AttrAmount::datum_desc().clone(), - AttrCreatedAt::datum_desc().clone(), - ]); + fn get_field_value(&self, field: &str) -> RS> { + match field { + + ORDER_ID => { + attr_field_access::attr_get_value::<_>(self.order_id.get()) } - &TUPLE_DESC - } - - fn object_name() -> &'static str { - ORDERS - } - - fn get_field_binary(&self, field: &str) -> RS>> { - match field { - ORDER_ID => attr_field_access::attr_get_binary::<_>(self.order_id.get()), - - USER_ID => attr_field_access::attr_get_binary::<_>(self.user_id.get()), - - MERCH_ID => attr_field_access::attr_get_binary::<_>(self.merch_id.get()), - - AMOUNT => attr_field_access::attr_get_binary::<_>(self.amount.get()), - - CREATED_AT => attr_field_access::attr_get_binary::<_>(self.created_at.get()), - - _ => { - panic!("unknown name"); - } + + USER_ID => { + attr_field_access::attr_get_value::<_>(self.user_id.get()) } - } - - fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { - match field { - ORDER_ID => { - attr_field_access::attr_set_binary::<_, _>( - self.order_id.get_mut(), - binary.as_ref(), - )?; - } - - USER_ID => { - attr_field_access::attr_set_binary::<_, _>( - self.user_id.get_mut(), - binary.as_ref(), - )?; - } - - MERCH_ID => { - attr_field_access::attr_set_binary::<_, _>( - self.merch_id.get_mut(), - binary.as_ref(), - )?; - } - - AMOUNT => { - attr_field_access::attr_set_binary::<_, _>( - self.amount.get_mut(), - binary.as_ref(), - )?; - } - - CREATED_AT => { - attr_field_access::attr_set_binary::<_, _>( - self.created_at.get_mut(), - binary.as_ref(), - )?; - } - - _ => { - panic!("unknown name"); - } + + MERCH_ID => { + attr_field_access::attr_get_value::<_>(self.merch_id.get()) + } + + AMOUNT => { + attr_field_access::attr_get_value::<_>(self.amount.get()) + } + + CREATED_AT => { + attr_field_access::attr_get_value::<_>(self.created_at.get()) } - Ok(()) + + _ => { panic!("unknown name"); } } + } - fn get_field_value(&self, field: &str) -> RS> { - match field { - ORDER_ID => attr_field_access::attr_get_value::<_>(self.order_id.get()), - - USER_ID => attr_field_access::attr_get_value::<_>(self.user_id.get()), - - MERCH_ID => attr_field_access::attr_get_value::<_>(self.merch_id.get()), - - AMOUNT => attr_field_access::attr_get_value::<_>(self.amount.get()), - - CREATED_AT => attr_field_access::attr_get_value::<_>(self.created_at.get()), - - _ => { - panic!("unknown name"); - } + fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { + match field { + + ORDER_ID => { + attr_field_access::attr_set_value::<_, _>(self.order_id.get_mut(), value)?; } + + USER_ID => { + attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; + } + + MERCH_ID => { + attr_field_access::attr_set_value::<_, _>(self.merch_id.get_mut(), value)?; + } + + AMOUNT => { + attr_field_access::attr_set_value::<_, _>(self.amount.get_mut(), value)?; + } + + CREATED_AT => { + attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; + } + + _ => { panic!("unknown name"); } } + Ok(()) + } +} - fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { - match field { - ORDER_ID => { - attr_field_access::attr_set_value::<_, _>(self.order_id.get_mut(), value)?; - } - - USER_ID => { - attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; - } - - MERCH_ID => { - attr_field_access::attr_set_value::<_, _>(self.merch_id.get_mut(), value)?; - } - - AMOUNT => { - attr_field_access::attr_set_value::<_, _>(self.amount.get_mut(), value)?; - } - CREATED_AT => { - attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrOrderId { + is_dirty:bool, + value: Option +} - _ => { - panic!("unknown name"); - } - } - Ok(()) +impl AttrOrderId { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrOrderId { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrOrderId { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrOrderId { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrOrderId { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + ORDERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + ORDER_ID + } +} - fn object_name() -> &'static str { - ORDERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrUserId { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - ORDER_ID +impl AttrUserId { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrUserId { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrUserId { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrUserId { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrUserId { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + ORDERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + USER_ID + } +} - fn object_name() -> &'static str { - ORDERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrMerchId { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - USER_ID +impl AttrMerchId { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrMerchId { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrMerchId { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrMerchId { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrMerchId { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + ORDERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + MERCH_ID + } +} - fn object_name() -> &'static str { - ORDERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrAmount { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - MERCH_ID +impl AttrAmount { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrAmount { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrAmount { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrAmount { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrAmount { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + ORDERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + AMOUNT + } +} - fn object_name() -> &'static str { - ORDERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrCreatedAt { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - AMOUNT +impl AttrCreatedAt { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrCreatedAt { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrCreatedAt { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrCreatedAt { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrCreatedAt { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + ORDERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + CREATED_AT + } +} - fn object_name() -> &'static str { - ORDERS - } - fn attr_name() -> &'static str { - CREATED_AT - } - } } diff --git a/example/wallet/src/rust/procedures.rs b/example/wallet/src/rust/procedures.rs index d52c65e..a8890d2 100644 --- a/example/wallet/src/rust/procedures.rs +++ b/example/wallet/src/rust/procedures.rs @@ -19,6 +19,14 @@ fn current_timestamp() -> i64 { seconds as _ } +fn required_balance(wallet: &Wallets) -> RS { + wallet + .get_balance() + .as_ref() + .copied() + .ok_or_else(|| m_error!(MuduError, "wallet balance is null")) +} + /**mudu-proc**/ pub fn transfer_funds(xid: XID, from_user_id: i32, to_user_id: i32, amount: i32) -> RS<()> { // Check amount > 0 @@ -50,6 +58,7 @@ pub fn transfer_funds(xid: XID, from_user_id: i32, to_user_id: i32, amount: i32) if *from_wallet.get_balance().as_ref().unwrap() < amount { return Err(m_error!(MuduError, "insufficient funds")); } + let from_balance = required_balance(&from_wallet)?; // Check the user account existing let to_wallet = mudu_query::( @@ -62,13 +71,14 @@ pub fn transfer_funds(xid: XID, from_user_id: i32, to_user_id: i32, amount: i32) } else { return Err(m_error!(MuduError, "no such user")); }; + let to_balance = required_balance(&_to_wallet)?; // Perform a transfer operation // 1. Deduct the balance of the account transferred out let deduct_updated_rows = mudu_command( xid, - sql_stmt!(&"UPDATE wallets SET balance = balance - ? WHERE user_id = ?;"), - sql_params!(&(amount, from_user_id)), + sql_stmt!(&"UPDATE wallets SET balance = ? WHERE user_id = ?;"), + sql_params!(&(from_balance - amount, from_user_id)), )?; if deduct_updated_rows != 1 { return Err(m_error!(MuduError, "transfer fund failed")); @@ -76,8 +86,8 @@ pub fn transfer_funds(xid: XID, from_user_id: i32, to_user_id: i32, amount: i32) // 2. Increase the balance of the transfer-in account let increase_updated_rows = mudu_command( xid, - sql_stmt!(&"UPDATE wallets SET balance = balance + ? WHERE user_id = ?;"), - sql_params!(&(amount, to_user_id)), + sql_stmt!(&"UPDATE wallets SET balance = ? WHERE user_id = ?;"), + sql_params!(&(to_balance + amount, to_user_id)), )?; if increase_updated_rows != 1 { return Err(m_error!(MuduError, "transfer fund failed")); @@ -208,12 +218,20 @@ pub fn deposit(xid: XID, user_id: i32, amount: i32) -> RS<()> { let now = current_timestamp(); let tx_id = mudu_sys::random::next_uuid_v4_string(); + let wallet = mudu_query::( + xid, + sql_stmt!(&"SELECT user_id, balance, updated_at FROM wallets WHERE user_id = ?"), + sql_params!(&(user_id,)), + )? + .next_record()? + .ok_or_else(|| m_error!(MuduError, "User wallet not found"))?; + let next_balance = required_balance(&wallet)? + amount; // Update wallet balance let updated = mudu_command( xid, - sql_stmt!(&"UPDATE wallets SET balance = balance + ?, updated_at = ? WHERE user_id = ?"), - sql_params!(&(amount, now, user_id)), + sql_stmt!(&"UPDATE wallets SET balance = ?, updated_at = ? WHERE user_id = ?"), + sql_params!(&(next_balance, now, user_id)), )?; if updated != 1 { @@ -255,12 +273,13 @@ pub fn withdraw(xid: XID, user_id: i32, amount: i32) -> RS<()> { let now = current_timestamp(); let tx_id = mudu_sys::random::next_uuid_v4_string(); + let next_balance = required_balance(&wallet)? - amount; // Update wallet balance mudu_command( xid, - sql_stmt!(&"UPDATE wallets SET balance = balance - ?, updated_at = ? WHERE user_id = ?"), - sql_params!(&(amount, now, user_id)), + sql_stmt!(&"UPDATE wallets SET balance = ?, updated_at = ? WHERE user_id = ?"), + sql_params!(&(next_balance, now, user_id)), )?; // Entity transaction @@ -297,19 +316,18 @@ pub fn transfer(xid: XID, from_user_id: i32, to_user_id: i32, amount: i32) -> RS if *sender_wallet.get_balance().as_ref().unwrap() < amount { return Err(m_error!(MuduError, "Insufficient funds")); } + let sender_balance = required_balance(&sender_wallet)?; // Check receiver exists - let receiver_exists = mudu_query::( + let receiver_wallet = mudu_query::( xid, sql_stmt!(&"SELECT user_id, balance, updated_at FROM wallets WHERE user_id = ?"), sql_params!(&(to_user_id.clone(),)), )? .next_record()? - .is_some(); + .ok_or_else(|| m_error!(MuduError, "Receiver wallet not found"))?; - if !receiver_exists { - return Err(m_error!(MuduError, "Receiver wallet not found")); - } + let receiver_balance = required_balance(&receiver_wallet)?; let now = current_timestamp(); let tx_id = mudu_sys::random::next_uuid_v4_string(); @@ -317,15 +335,15 @@ pub fn transfer(xid: XID, from_user_id: i32, to_user_id: i32, amount: i32) -> RS // Debit sender mudu_command( xid, - sql_stmt!(&"UPDATE wallets SET balance = balance - ?, updated_at = ? WHERE user_id = ?"), - sql_params!(&(amount, now, from_user_id)), + sql_stmt!(&"UPDATE wallets SET balance = ?, updated_at = ? WHERE user_id = ?"), + sql_params!(&(sender_balance - amount, now, from_user_id)), )?; // Credit receiver mudu_command( xid, - sql_stmt!(&"UPDATE wallets SET balance = balance + ?, updated_at = ? WHERE user_id = ?"), - sql_params!(&(amount, now, to_user_id)), + sql_stmt!(&"UPDATE wallets SET balance = ?, updated_at = ? WHERE user_id = ?"), + sql_params!(&(receiver_balance + amount, now, to_user_id)), )?; // Entity transaction @@ -369,12 +387,13 @@ pub fn purchase(xid: XID, user_id: i32, amount: i32, description: String) -> RS< let now = current_timestamp(); let tx_id = mudu_sys::random::next_uuid_v4_string(); + let next_balance = required_balance(&wallet)? - amount; // Deduct amount mudu_command( xid, - sql_stmt!(&"UPDATE wallets SET balance = balance - ?, updated_at = ? WHERE user_id = ?"), - sql_params!(&(amount, now, user_id)), + sql_stmt!(&"UPDATE wallets SET balance = ?, updated_at = ? WHERE user_id = ?"), + sql_params!(&(next_balance, now, user_id)), )?; // Entity transaction diff --git a/example/wallet/src/rust/transactions.rs b/example/wallet/src/rust/transactions.rs index 62c52ff..448414c 100644 --- a/example/wallet/src/rust/transactions.rs +++ b/example/wallet/src/rust/transactions.rs @@ -1,640 +1,690 @@ pub mod object { - use lazy_static::lazy_static; - use mudu::common::result::RS; - use mudu_contract::database::attr_field_access; - use mudu_contract::database::attr_value::AttrValue; - use mudu_contract::database::entity::Entity; - use mudu_contract::database::entity_utils; - use mudu_contract::database::sql_params::SQLParamMarker; - use mudu_contract::tuple::datum_desc::DatumDesc; - use mudu_contract::tuple::tuple_datum::TupleDatumMarker; - use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; - use mudu_type::dat_binary::DatBinary; - use mudu_type::dat_textual::DatTextual; - use mudu_type::dat_type::DatType; - use mudu_type::dat_type_id::DatTypeID; - use mudu_type::dat_value::DatValue; - use mudu_type::datum::{Datum, DatumDyn}; - - // constant definition - const TRANSACTIONS: &str = "transactions"; - - const TRANS_ID: &str = "trans_id"; - - const TRANS_TYPE: &str = "trans_type"; - - const FROM_USER: &str = "from_user"; - - const TO_USER: &str = "to_user"; - - const AMOUNT: &str = "amount"; - - const CREATED_AT: &str = "created_at"; - - // entity struct definition - #[derive(Debug, Clone, Default)] - pub struct Transactions { - trans_id: AttrTransId, - - trans_type: AttrTransType, - - from_user: AttrFromUser, - - to_user: AttrToUser, - - amount: AttrAmount, +use lazy_static::lazy_static; +use mudu::common::result::RS; +use mudu_type::dat_binary::DatBinary; +use mudu_type::dat_textual::DatTextual; +use mudu_type::dat_type::DatType; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dat_value::DatValue; +use mudu_type::datum::{Datum, DatumDyn}; +use mudu_contract::database::attr_field_access; +use mudu_contract::database::attr_value::AttrValue; +use mudu_contract::database::entity::Entity; +use mudu_contract::database::entity_utils; +use mudu_contract::tuple::datum_desc::DatumDesc; +use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; +use mudu_contract::tuple::tuple_datum::TupleDatumMarker; +use mudu_contract::database::sql_params::SQLParamMarker; + +// constant definition +const TRANSACTIONS:&str = "transactions"; + +const TRANS_ID:&str = "trans_id"; + +const TRANS_TYPE:&str = "trans_type"; + +const FROM_USER:&str = "from_user"; + +const TO_USER:&str = "to_user"; + +const AMOUNT:&str = "amount"; + +const CREATED_AT:&str = "created_at"; + + +// entity struct definition +#[derive(Debug, Clone, Default)] +pub struct Transactions { + + trans_id: AttrTransId, + + trans_type: AttrTransType, + + from_user: AttrFromUser, + + to_user: AttrToUser, + + amount: AttrAmount, + + created_at: AttrCreatedAt, + +} - created_at: AttrCreatedAt, +impl TupleDatumMarker for Transactions {} + +impl SQLParamMarker for Transactions {} + +impl Transactions { + pub fn new( + trans_id: Option, + trans_type: Option, + from_user: Option, + to_user: Option, + amount: Option, + created_at: Option, + + ) -> Self { + let s = Self { + + trans_id : AttrTransId::from(trans_id), + + trans_type : AttrTransType::from(trans_type), + + from_user : AttrFromUser::from(from_user), + + to_user : AttrToUser::from(to_user), + + amount : AttrAmount::from(amount), + + created_at : AttrCreatedAt::from(created_at), + + }; + s } - impl TupleDatumMarker for Transactions {} - - impl SQLParamMarker for Transactions {} + pub fn new_empty() -> Self { + Self::default() + } - impl Transactions { - pub fn new( - trans_id: Option, - trans_type: Option, - from_user: Option, - to_user: Option, - amount: Option, - created_at: Option, - ) -> Self { - let s = Self { - trans_id: AttrTransId::from(trans_id), + + pub fn set_trans_id( + &mut self, + trans_id: String, + ) { + self.trans_id.update(trans_id) + } - trans_type: AttrTransType::from(trans_type), + pub fn get_trans_id( + &self, + ) -> &Option { + self.trans_id.get() + } + + pub fn set_trans_type( + &mut self, + trans_type: String, + ) { + self.trans_type.update(trans_type) + } - from_user: AttrFromUser::from(from_user), + pub fn get_trans_type( + &self, + ) -> &Option { + self.trans_type.get() + } + + pub fn set_from_user( + &mut self, + from_user: i32, + ) { + self.from_user.update(from_user) + } - to_user: AttrToUser::from(to_user), + pub fn get_from_user( + &self, + ) -> &Option { + self.from_user.get() + } + + pub fn set_to_user( + &mut self, + to_user: i32, + ) { + self.to_user.update(to_user) + } - amount: AttrAmount::from(amount), + pub fn get_to_user( + &self, + ) -> &Option { + self.to_user.get() + } + + pub fn set_amount( + &mut self, + amount: i32, + ) { + self.amount.update(amount) + } - created_at: AttrCreatedAt::from(created_at), - }; - s - } + pub fn get_amount( + &self, + ) -> &Option { + self.amount.get() + } + + pub fn set_created_at( + &mut self, + created_at: i32, + ) { + self.created_at.update(created_at) + } - pub fn new_empty() -> Self { - Self::default() - } + pub fn get_created_at( + &self, + ) -> &Option { + self.created_at.get() + } + +} - pub fn set_trans_id(&mut self, trans_id: String) { - self.trans_id.update(trans_id) +impl Datum for Transactions { + fn dat_type() -> &'static DatType { + lazy_static! { + static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); } + &DAT_TYPE + } - pub fn get_trans_id(&self) -> &Option { - self.trans_id.get() - } + fn from_binary(binary: &[u8]) -> RS { + entity_utils::entity_from_binary(binary) + } - pub fn set_trans_type(&mut self, trans_type: String) { - self.trans_type.update(trans_type) - } + fn from_value(value: &DatValue) -> RS { + entity_utils::entity_from_value(value) + } - pub fn get_trans_type(&self) -> &Option { - self.trans_type.get() - } + fn from_textual(textual: &str) -> RS { + entity_utils::entity_from_textual(textual) + } +} - pub fn set_from_user(&mut self, from_user: i32) { - self.from_user.update(from_user) - } +impl DatumDyn for Transactions { + fn dat_type_id(&self) -> RS { + entity_utils::entity_dat_type_id() + } - pub fn get_from_user(&self) -> &Option { - self.from_user.get() - } + fn to_binary(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_binary(self, dat_type) + } - pub fn set_to_user(&mut self, to_user: i32) { - self.to_user.update(to_user) - } + fn to_textual(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_textual(self, dat_type) + } - pub fn get_to_user(&self) -> &Option { - self.to_user.get() - } + fn to_value(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_value(self, dat_type) + } - pub fn set_amount(&mut self, amount: i32) { - self.amount.update(amount) - } + fn clone_boxed(&self) -> Box { + entity_utils::entity_clone_boxed(self) + } +} - pub fn get_amount(&self) -> &Option { - self.amount.get() - } +impl Entity for Transactions { + fn new_empty() -> Self { + Self::new_empty() + } - pub fn set_created_at(&mut self, created_at: i32) { - self.created_at.update(created_at) - } + fn tuple_desc() -> &'static TupleFieldDesc { + lazy_static! { + static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ + + AttrTransId::datum_desc().clone(), + + AttrTransType::datum_desc().clone(), + + AttrFromUser::datum_desc().clone(), + + AttrToUser::datum_desc().clone(), + + AttrAmount::datum_desc().clone(), + + AttrCreatedAt::datum_desc().clone(), + + ]); + } + &TUPLE_DESC + } - pub fn get_created_at(&self) -> &Option { - self.created_at.get() - } + fn object_name() -> &'static str { + TRANSACTIONS } - impl Datum for Transactions { - fn dat_type() -> &'static DatType { - lazy_static! { - static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); + fn get_field_binary(&self, field: &str) -> RS>> { + match field { + + TRANS_ID => { + attr_field_access::attr_get_binary::<_>(self.trans_id.get()) } - &DAT_TYPE - } - - fn from_binary(binary: &[u8]) -> RS { - entity_utils::entity_from_binary(binary) - } - - fn from_value(value: &DatValue) -> RS { - entity_utils::entity_from_value(value) - } - - fn from_textual(textual: &str) -> RS { - entity_utils::entity_from_textual(textual) + + TRANS_TYPE => { + attr_field_access::attr_get_binary::<_>(self.trans_type.get()) + } + + FROM_USER => { + attr_field_access::attr_get_binary::<_>(self.from_user.get()) + } + + TO_USER => { + attr_field_access::attr_get_binary::<_>(self.to_user.get()) + } + + AMOUNT => { + attr_field_access::attr_get_binary::<_>(self.amount.get()) + } + + CREATED_AT => { + attr_field_access::attr_get_binary::<_>(self.created_at.get()) + } + + _ => { panic!("unknown name"); } } } - impl DatumDyn for Transactions { - fn dat_type_id(&self) -> RS { - entity_utils::entity_dat_type_id() - } - - fn to_binary(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_binary(self, dat_type) - } - - fn to_textual(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_textual(self, dat_type) - } - - fn to_value(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_value(self, dat_type) - } - - fn clone_boxed(&self) -> Box { - entity_utils::entity_clone_boxed(self) + fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { + match field { + + TRANS_ID => { + attr_field_access::attr_set_binary::<_, _>(self.trans_id.get_mut(), binary.as_ref())?; + } + + TRANS_TYPE => { + attr_field_access::attr_set_binary::<_, _>(self.trans_type.get_mut(), binary.as_ref())?; + } + + FROM_USER => { + attr_field_access::attr_set_binary::<_, _>(self.from_user.get_mut(), binary.as_ref())?; + } + + TO_USER => { + attr_field_access::attr_set_binary::<_, _>(self.to_user.get_mut(), binary.as_ref())?; + } + + AMOUNT => { + attr_field_access::attr_set_binary::<_, _>(self.amount.get_mut(), binary.as_ref())?; + } + + CREATED_AT => { + attr_field_access::attr_set_binary::<_, _>(self.created_at.get_mut(), binary.as_ref())?; + } + + _ => { panic!("unknown name"); } } + Ok(()) } - impl Entity for Transactions { - fn new_empty() -> Self { - Self::new_empty() - } - - fn tuple_desc() -> &'static TupleFieldDesc { - lazy_static! { - static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ - AttrTransId::datum_desc().clone(), - AttrTransType::datum_desc().clone(), - AttrFromUser::datum_desc().clone(), - AttrToUser::datum_desc().clone(), - AttrAmount::datum_desc().clone(), - AttrCreatedAt::datum_desc().clone(), - ]); + fn get_field_value(&self, field: &str) -> RS> { + match field { + + TRANS_ID => { + attr_field_access::attr_get_value::<_>(self.trans_id.get()) } - &TUPLE_DESC - } - - fn object_name() -> &'static str { - TRANSACTIONS - } - - fn get_field_binary(&self, field: &str) -> RS>> { - match field { - TRANS_ID => attr_field_access::attr_get_binary::<_>(self.trans_id.get()), - - TRANS_TYPE => attr_field_access::attr_get_binary::<_>(self.trans_type.get()), - - FROM_USER => attr_field_access::attr_get_binary::<_>(self.from_user.get()), - - TO_USER => attr_field_access::attr_get_binary::<_>(self.to_user.get()), - - AMOUNT => attr_field_access::attr_get_binary::<_>(self.amount.get()), - - CREATED_AT => attr_field_access::attr_get_binary::<_>(self.created_at.get()), - - _ => { - panic!("unknown name"); - } + + TRANS_TYPE => { + attr_field_access::attr_get_value::<_>(self.trans_type.get()) } - } - - fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { - match field { - TRANS_ID => { - attr_field_access::attr_set_binary::<_, _>( - self.trans_id.get_mut(), - binary.as_ref(), - )?; - } - - TRANS_TYPE => { - attr_field_access::attr_set_binary::<_, _>( - self.trans_type.get_mut(), - binary.as_ref(), - )?; - } - - FROM_USER => { - attr_field_access::attr_set_binary::<_, _>( - self.from_user.get_mut(), - binary.as_ref(), - )?; - } - - TO_USER => { - attr_field_access::attr_set_binary::<_, _>( - self.to_user.get_mut(), - binary.as_ref(), - )?; - } - - AMOUNT => { - attr_field_access::attr_set_binary::<_, _>( - self.amount.get_mut(), - binary.as_ref(), - )?; - } - - CREATED_AT => { - attr_field_access::attr_set_binary::<_, _>( - self.created_at.get_mut(), - binary.as_ref(), - )?; - } - - _ => { - panic!("unknown name"); - } + + FROM_USER => { + attr_field_access::attr_get_value::<_>(self.from_user.get()) + } + + TO_USER => { + attr_field_access::attr_get_value::<_>(self.to_user.get()) + } + + AMOUNT => { + attr_field_access::attr_get_value::<_>(self.amount.get()) + } + + CREATED_AT => { + attr_field_access::attr_get_value::<_>(self.created_at.get()) } - Ok(()) + + _ => { panic!("unknown name"); } } + } - fn get_field_value(&self, field: &str) -> RS> { - match field { - TRANS_ID => attr_field_access::attr_get_value::<_>(self.trans_id.get()), - - TRANS_TYPE => attr_field_access::attr_get_value::<_>(self.trans_type.get()), - - FROM_USER => attr_field_access::attr_get_value::<_>(self.from_user.get()), - - TO_USER => attr_field_access::attr_get_value::<_>(self.to_user.get()), - - AMOUNT => attr_field_access::attr_get_value::<_>(self.amount.get()), - - CREATED_AT => attr_field_access::attr_get_value::<_>(self.created_at.get()), - - _ => { - panic!("unknown name"); - } + fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { + match field { + + TRANS_ID => { + attr_field_access::attr_set_value::<_, _>(self.trans_id.get_mut(), value)?; + } + + TRANS_TYPE => { + attr_field_access::attr_set_value::<_, _>(self.trans_type.get_mut(), value)?; + } + + FROM_USER => { + attr_field_access::attr_set_value::<_, _>(self.from_user.get_mut(), value)?; + } + + TO_USER => { + attr_field_access::attr_set_value::<_, _>(self.to_user.get_mut(), value)?; } + + AMOUNT => { + attr_field_access::attr_set_value::<_, _>(self.amount.get_mut(), value)?; + } + + CREATED_AT => { + attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; + } + + _ => { panic!("unknown name"); } } + Ok(()) + } +} - fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { - match field { - TRANS_ID => { - attr_field_access::attr_set_value::<_, _>(self.trans_id.get_mut(), value)?; - } - - TRANS_TYPE => { - attr_field_access::attr_set_value::<_, _>(self.trans_type.get_mut(), value)?; - } - - FROM_USER => { - attr_field_access::attr_set_value::<_, _>(self.from_user.get_mut(), value)?; - } - - TO_USER => { - attr_field_access::attr_set_value::<_, _>(self.to_user.get_mut(), value)?; - } - - AMOUNT => { - attr_field_access::attr_set_value::<_, _>(self.amount.get_mut(), value)?; - } - CREATED_AT => { - attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrTransId { + is_dirty:bool, + value: Option +} - _ => { - panic!("unknown name"); - } - } - Ok(()) +impl AttrTransId { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrTransId { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrTransId { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrTransId { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrTransId { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + TRANS_ID + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrTransType { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - TRANS_ID +impl AttrTransType { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrTransType { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrTransType { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrTransType { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrTransType { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + TRANS_TYPE + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrFromUser { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - TRANS_TYPE +impl AttrFromUser { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrFromUser { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrFromUser { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrFromUser { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrFromUser { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + FROM_USER + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrToUser { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - FROM_USER +impl AttrToUser { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrToUser { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrToUser { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrToUser { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrToUser { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + TO_USER + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrAmount { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - TO_USER +impl AttrAmount { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrAmount { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrAmount { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrAmount { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrAmount { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + AMOUNT + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrCreatedAt { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - AMOUNT +impl AttrCreatedAt { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrCreatedAt { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrCreatedAt { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrCreatedAt { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrCreatedAt { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + TRANSACTIONS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + CREATED_AT + } +} - fn object_name() -> &'static str { - TRANSACTIONS - } - fn attr_name() -> &'static str { - CREATED_AT - } - } } diff --git a/example/wallet/src/rust/users.rs b/example/wallet/src/rust/users.rs index a007e74..e84a9cf 100644 --- a/example/wallet/src/rust/users.rs +++ b/example/wallet/src/rust/users.rs @@ -1,724 +1,781 @@ pub mod object { - use lazy_static::lazy_static; - use mudu::common::result::RS; - use mudu_contract::database::attr_field_access; - use mudu_contract::database::attr_value::AttrValue; - use mudu_contract::database::entity::Entity; - use mudu_contract::database::entity_utils; - use mudu_contract::database::sql_params::SQLParamMarker; - use mudu_contract::tuple::datum_desc::DatumDesc; - use mudu_contract::tuple::tuple_datum::TupleDatumMarker; - use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; - use mudu_type::dat_binary::DatBinary; - use mudu_type::dat_textual::DatTextual; - use mudu_type::dat_type::DatType; - use mudu_type::dat_type_id::DatTypeID; - use mudu_type::dat_value::DatValue; - use mudu_type::datum::{Datum, DatumDyn}; - - // constant definition - const USERS: &str = "users"; - - const USER_ID: &str = "user_id"; - - const NAME: &str = "name"; - - const PHONE: &str = "phone"; - - const EMAIL: &str = "email"; - - const PASSWORD: &str = "password"; - - const CREATED_AT: &str = "created_at"; - - const UPDATED_AT: &str = "updated_at"; - - // entity struct definition - #[derive(Debug, Clone, Default)] - pub struct Users { - user_id: AttrUserId, - - name: AttrName, - - phone: AttrPhone, - - email: AttrEmail, - - password: AttrPassword, - - created_at: AttrCreatedAt, +use lazy_static::lazy_static; +use mudu::common::result::RS; +use mudu_type::dat_binary::DatBinary; +use mudu_type::dat_textual::DatTextual; +use mudu_type::dat_type::DatType; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dat_value::DatValue; +use mudu_type::datum::{Datum, DatumDyn}; +use mudu_contract::database::attr_field_access; +use mudu_contract::database::attr_value::AttrValue; +use mudu_contract::database::entity::Entity; +use mudu_contract::database::entity_utils; +use mudu_contract::tuple::datum_desc::DatumDesc; +use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; +use mudu_contract::tuple::tuple_datum::TupleDatumMarker; +use mudu_contract::database::sql_params::SQLParamMarker; + +// constant definition +const USERS:&str = "users"; + +const USER_ID:&str = "user_id"; + +const NAME:&str = "name"; + +const PHONE:&str = "phone"; + +const EMAIL:&str = "email"; + +const PASSWORD:&str = "password"; + +const CREATED_AT:&str = "created_at"; + +const UPDATED_AT:&str = "updated_at"; + + +// entity struct definition +#[derive(Debug, Clone, Default)] +pub struct Users { + + user_id: AttrUserId, + + name: AttrName, + + phone: AttrPhone, + + email: AttrEmail, + + password: AttrPassword, + + created_at: AttrCreatedAt, + + updated_at: AttrUpdatedAt, + +} - updated_at: AttrUpdatedAt, +impl TupleDatumMarker for Users {} + +impl SQLParamMarker for Users {} + +impl Users { + pub fn new( + user_id: Option, + name: Option, + phone: Option, + email: Option, + password: Option, + created_at: Option, + updated_at: Option, + + ) -> Self { + let s = Self { + + user_id : AttrUserId::from(user_id), + + name : AttrName::from(name), + + phone : AttrPhone::from(phone), + + email : AttrEmail::from(email), + + password : AttrPassword::from(password), + + created_at : AttrCreatedAt::from(created_at), + + updated_at : AttrUpdatedAt::from(updated_at), + + }; + s } - impl TupleDatumMarker for Users {} - - impl SQLParamMarker for Users {} - - impl Users { - pub fn new( - user_id: Option, - name: Option, - phone: Option, - email: Option, - password: Option, - created_at: Option, - updated_at: Option, - ) -> Self { - let s = Self { - user_id: AttrUserId::from(user_id), - - name: AttrName::from(name), + pub fn new_empty() -> Self { + Self::default() + } - phone: AttrPhone::from(phone), + + pub fn set_user_id( + &mut self, + user_id: i32, + ) { + self.user_id.update(user_id) + } - email: AttrEmail::from(email), + pub fn get_user_id( + &self, + ) -> &Option { + self.user_id.get() + } + + pub fn set_name( + &mut self, + name: String, + ) { + self.name.update(name) + } - password: AttrPassword::from(password), + pub fn get_name( + &self, + ) -> &Option { + self.name.get() + } + + pub fn set_phone( + &mut self, + phone: String, + ) { + self.phone.update(phone) + } - created_at: AttrCreatedAt::from(created_at), + pub fn get_phone( + &self, + ) -> &Option { + self.phone.get() + } + + pub fn set_email( + &mut self, + email: String, + ) { + self.email.update(email) + } - updated_at: AttrUpdatedAt::from(updated_at), - }; - s - } + pub fn get_email( + &self, + ) -> &Option { + self.email.get() + } + + pub fn set_password( + &mut self, + password: String, + ) { + self.password.update(password) + } - pub fn new_empty() -> Self { - Self::default() - } + pub fn get_password( + &self, + ) -> &Option { + self.password.get() + } + + pub fn set_created_at( + &mut self, + created_at: i32, + ) { + self.created_at.update(created_at) + } - pub fn set_user_id(&mut self, user_id: i32) { - self.user_id.update(user_id) - } + pub fn get_created_at( + &self, + ) -> &Option { + self.created_at.get() + } + + pub fn set_updated_at( + &mut self, + updated_at: i32, + ) { + self.updated_at.update(updated_at) + } - pub fn get_user_id(&self) -> &Option { - self.user_id.get() - } + pub fn get_updated_at( + &self, + ) -> &Option { + self.updated_at.get() + } + +} - pub fn set_name(&mut self, name: String) { - self.name.update(name) +impl Datum for Users { + fn dat_type() -> &'static DatType { + lazy_static! { + static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); } + &DAT_TYPE + } - pub fn get_name(&self) -> &Option { - self.name.get() - } + fn from_binary(binary: &[u8]) -> RS { + entity_utils::entity_from_binary(binary) + } - pub fn set_phone(&mut self, phone: String) { - self.phone.update(phone) - } + fn from_value(value: &DatValue) -> RS { + entity_utils::entity_from_value(value) + } - pub fn get_phone(&self) -> &Option { - self.phone.get() - } + fn from_textual(textual: &str) -> RS { + entity_utils::entity_from_textual(textual) + } +} - pub fn set_email(&mut self, email: String) { - self.email.update(email) - } +impl DatumDyn for Users { + fn dat_type_id(&self) -> RS { + entity_utils::entity_dat_type_id() + } - pub fn get_email(&self) -> &Option { - self.email.get() - } + fn to_binary(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_binary(self, dat_type) + } - pub fn set_password(&mut self, password: String) { - self.password.update(password) - } + fn to_textual(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_textual(self, dat_type) + } - pub fn get_password(&self) -> &Option { - self.password.get() - } + fn to_value(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_value(self, dat_type) + } - pub fn set_created_at(&mut self, created_at: i32) { - self.created_at.update(created_at) - } + fn clone_boxed(&self) -> Box { + entity_utils::entity_clone_boxed(self) + } +} - pub fn get_created_at(&self) -> &Option { - self.created_at.get() - } +impl Entity for Users { + fn new_empty() -> Self { + Self::new_empty() + } - pub fn set_updated_at(&mut self, updated_at: i32) { - self.updated_at.update(updated_at) - } + fn tuple_desc() -> &'static TupleFieldDesc { + lazy_static! { + static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ + + AttrUserId::datum_desc().clone(), + + AttrName::datum_desc().clone(), + + AttrPhone::datum_desc().clone(), + + AttrEmail::datum_desc().clone(), + + AttrPassword::datum_desc().clone(), + + AttrCreatedAt::datum_desc().clone(), + + AttrUpdatedAt::datum_desc().clone(), + + ]); + } + &TUPLE_DESC + } - pub fn get_updated_at(&self) -> &Option { - self.updated_at.get() - } + fn object_name() -> &'static str { + USERS } - impl Datum for Users { - fn dat_type() -> &'static DatType { - lazy_static! { - static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); + fn get_field_binary(&self, field: &str) -> RS>> { + match field { + + USER_ID => { + attr_field_access::attr_get_binary::<_>(self.user_id.get()) } - &DAT_TYPE - } - - fn from_binary(binary: &[u8]) -> RS { - entity_utils::entity_from_binary(binary) - } - - fn from_value(value: &DatValue) -> RS { - entity_utils::entity_from_value(value) - } - - fn from_textual(textual: &str) -> RS { - entity_utils::entity_from_textual(textual) + + NAME => { + attr_field_access::attr_get_binary::<_>(self.name.get()) + } + + PHONE => { + attr_field_access::attr_get_binary::<_>(self.phone.get()) + } + + EMAIL => { + attr_field_access::attr_get_binary::<_>(self.email.get()) + } + + PASSWORD => { + attr_field_access::attr_get_binary::<_>(self.password.get()) + } + + CREATED_AT => { + attr_field_access::attr_get_binary::<_>(self.created_at.get()) + } + + UPDATED_AT => { + attr_field_access::attr_get_binary::<_>(self.updated_at.get()) + } + + _ => { panic!("unknown name"); } } } - impl DatumDyn for Users { - fn dat_type_id(&self) -> RS { - entity_utils::entity_dat_type_id() - } - - fn to_binary(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_binary(self, dat_type) - } - - fn to_textual(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_textual(self, dat_type) - } - - fn to_value(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_value(self, dat_type) - } - - fn clone_boxed(&self) -> Box { - entity_utils::entity_clone_boxed(self) + fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { + match field { + + USER_ID => { + attr_field_access::attr_set_binary::<_, _>(self.user_id.get_mut(), binary.as_ref())?; + } + + NAME => { + attr_field_access::attr_set_binary::<_, _>(self.name.get_mut(), binary.as_ref())?; + } + + PHONE => { + attr_field_access::attr_set_binary::<_, _>(self.phone.get_mut(), binary.as_ref())?; + } + + EMAIL => { + attr_field_access::attr_set_binary::<_, _>(self.email.get_mut(), binary.as_ref())?; + } + + PASSWORD => { + attr_field_access::attr_set_binary::<_, _>(self.password.get_mut(), binary.as_ref())?; + } + + CREATED_AT => { + attr_field_access::attr_set_binary::<_, _>(self.created_at.get_mut(), binary.as_ref())?; + } + + UPDATED_AT => { + attr_field_access::attr_set_binary::<_, _>(self.updated_at.get_mut(), binary.as_ref())?; + } + + _ => { panic!("unknown name"); } } + Ok(()) } - impl Entity for Users { - fn new_empty() -> Self { - Self::new_empty() - } - - fn tuple_desc() -> &'static TupleFieldDesc { - lazy_static! { - static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ - AttrUserId::datum_desc().clone(), - AttrName::datum_desc().clone(), - AttrPhone::datum_desc().clone(), - AttrEmail::datum_desc().clone(), - AttrPassword::datum_desc().clone(), - AttrCreatedAt::datum_desc().clone(), - AttrUpdatedAt::datum_desc().clone(), - ]); + fn get_field_value(&self, field: &str) -> RS> { + match field { + + USER_ID => { + attr_field_access::attr_get_value::<_>(self.user_id.get()) } - &TUPLE_DESC - } - - fn object_name() -> &'static str { - USERS - } - - fn get_field_binary(&self, field: &str) -> RS>> { - match field { - USER_ID => attr_field_access::attr_get_binary::<_>(self.user_id.get()), - - NAME => attr_field_access::attr_get_binary::<_>(self.name.get()), - - PHONE => attr_field_access::attr_get_binary::<_>(self.phone.get()), - - EMAIL => attr_field_access::attr_get_binary::<_>(self.email.get()), - - PASSWORD => attr_field_access::attr_get_binary::<_>(self.password.get()), - - CREATED_AT => attr_field_access::attr_get_binary::<_>(self.created_at.get()), - - UPDATED_AT => attr_field_access::attr_get_binary::<_>(self.updated_at.get()), - - _ => { - panic!("unknown name"); - } + + NAME => { + attr_field_access::attr_get_value::<_>(self.name.get()) } - } - - fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { - match field { - USER_ID => { - attr_field_access::attr_set_binary::<_, _>( - self.user_id.get_mut(), - binary.as_ref(), - )?; - } - - NAME => { - attr_field_access::attr_set_binary::<_, _>( - self.name.get_mut(), - binary.as_ref(), - )?; - } - - PHONE => { - attr_field_access::attr_set_binary::<_, _>( - self.phone.get_mut(), - binary.as_ref(), - )?; - } - - EMAIL => { - attr_field_access::attr_set_binary::<_, _>( - self.email.get_mut(), - binary.as_ref(), - )?; - } - - PASSWORD => { - attr_field_access::attr_set_binary::<_, _>( - self.password.get_mut(), - binary.as_ref(), - )?; - } - - CREATED_AT => { - attr_field_access::attr_set_binary::<_, _>( - self.created_at.get_mut(), - binary.as_ref(), - )?; - } - - UPDATED_AT => { - attr_field_access::attr_set_binary::<_, _>( - self.updated_at.get_mut(), - binary.as_ref(), - )?; - } - - _ => { - panic!("unknown name"); - } + + PHONE => { + attr_field_access::attr_get_value::<_>(self.phone.get()) + } + + EMAIL => { + attr_field_access::attr_get_value::<_>(self.email.get()) } - Ok(()) + + PASSWORD => { + attr_field_access::attr_get_value::<_>(self.password.get()) + } + + CREATED_AT => { + attr_field_access::attr_get_value::<_>(self.created_at.get()) + } + + UPDATED_AT => { + attr_field_access::attr_get_value::<_>(self.updated_at.get()) + } + + _ => { panic!("unknown name"); } } + } - fn get_field_value(&self, field: &str) -> RS> { - match field { - USER_ID => attr_field_access::attr_get_value::<_>(self.user_id.get()), - - NAME => attr_field_access::attr_get_value::<_>(self.name.get()), - - PHONE => attr_field_access::attr_get_value::<_>(self.phone.get()), - - EMAIL => attr_field_access::attr_get_value::<_>(self.email.get()), - - PASSWORD => attr_field_access::attr_get_value::<_>(self.password.get()), - - CREATED_AT => attr_field_access::attr_get_value::<_>(self.created_at.get()), - - UPDATED_AT => attr_field_access::attr_get_value::<_>(self.updated_at.get()), - - _ => { - panic!("unknown name"); - } + fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { + match field { + + USER_ID => { + attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; + } + + NAME => { + attr_field_access::attr_set_value::<_, _>(self.name.get_mut(), value)?; + } + + PHONE => { + attr_field_access::attr_set_value::<_, _>(self.phone.get_mut(), value)?; + } + + EMAIL => { + attr_field_access::attr_set_value::<_, _>(self.email.get_mut(), value)?; + } + + PASSWORD => { + attr_field_access::attr_set_value::<_, _>(self.password.get_mut(), value)?; + } + + CREATED_AT => { + attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; } + + UPDATED_AT => { + attr_field_access::attr_set_value::<_, _>(self.updated_at.get_mut(), value)?; + } + + _ => { panic!("unknown name"); } } + Ok(()) + } +} - fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { - match field { - USER_ID => { - attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; - } - - NAME => { - attr_field_access::attr_set_value::<_, _>(self.name.get_mut(), value)?; - } - - PHONE => { - attr_field_access::attr_set_value::<_, _>(self.phone.get_mut(), value)?; - } - - EMAIL => { - attr_field_access::attr_set_value::<_, _>(self.email.get_mut(), value)?; - } - - PASSWORD => { - attr_field_access::attr_set_value::<_, _>(self.password.get_mut(), value)?; - } - - CREATED_AT => { - attr_field_access::attr_set_value::<_, _>(self.created_at.get_mut(), value)?; - } - UPDATED_AT => { - attr_field_access::attr_set_value::<_, _>(self.updated_at.get_mut(), value)?; - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrUserId { + is_dirty:bool, + value: Option +} - _ => { - panic!("unknown name"); - } - } - Ok(()) +impl AttrUserId { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrUserId { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrUserId { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrUserId { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrUserId { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + USER_ID + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrName { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - USER_ID +impl AttrName { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrName { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrName { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrName { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrName { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + NAME + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrPhone { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - NAME +impl AttrPhone { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrPhone { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrPhone { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrPhone { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrPhone { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + PHONE + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrEmail { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - PHONE +impl AttrEmail { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrEmail { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrEmail { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrEmail { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrEmail { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + EMAIL + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrPassword { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - EMAIL +impl AttrPassword { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrPassword { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrPassword { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: String) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrPassword { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: String) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrPassword { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + PASSWORD + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrCreatedAt { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - PASSWORD +impl AttrCreatedAt { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrCreatedAt { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrCreatedAt { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrCreatedAt { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrCreatedAt { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + CREATED_AT + } +} - fn object_name() -> &'static str { - USERS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrUpdatedAt { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - CREATED_AT +impl AttrUpdatedAt { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrUpdatedAt { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrUpdatedAt { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrUpdatedAt { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrUpdatedAt { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + USERS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + UPDATED_AT + } +} - fn object_name() -> &'static str { - USERS - } - fn attr_name() -> &'static str { - UPDATED_AT - } - } } diff --git a/example/wallet/src/rust/wallets.rs b/example/wallet/src/rust/wallets.rs index 420d8d6..dd775bc 100644 --- a/example/wallet/src/rust/wallets.rs +++ b/example/wallet/src/rust/wallets.rs @@ -1,384 +1,417 @@ pub mod object { - use lazy_static::lazy_static; - use mudu::common::result::RS; - use mudu_contract::database::attr_field_access; - use mudu_contract::database::attr_value::AttrValue; - use mudu_contract::database::entity::Entity; - use mudu_contract::database::entity_utils; - use mudu_contract::database::sql_params::SQLParamMarker; - use mudu_contract::tuple::datum_desc::DatumDesc; - use mudu_contract::tuple::tuple_datum::TupleDatumMarker; - use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; - use mudu_type::dat_binary::DatBinary; - use mudu_type::dat_textual::DatTextual; - use mudu_type::dat_type::DatType; - use mudu_type::dat_type_id::DatTypeID; - use mudu_type::dat_value::DatValue; - use mudu_type::datum::{Datum, DatumDyn}; - - // constant definition - const WALLETS: &str = "wallets"; - - const USER_ID: &str = "user_id"; - - const BALANCE: &str = "balance"; - - const UPDATED_AT: &str = "updated_at"; - - // entity struct definition - #[derive(Debug, Clone, Default)] - pub struct Wallets { - user_id: AttrUserId, - - balance: AttrBalance, +use lazy_static::lazy_static; +use mudu::common::result::RS; +use mudu_type::dat_binary::DatBinary; +use mudu_type::dat_textual::DatTextual; +use mudu_type::dat_type::DatType; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dat_value::DatValue; +use mudu_type::datum::{Datum, DatumDyn}; +use mudu_contract::database::attr_field_access; +use mudu_contract::database::attr_value::AttrValue; +use mudu_contract::database::entity::Entity; +use mudu_contract::database::entity_utils; +use mudu_contract::tuple::datum_desc::DatumDesc; +use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; +use mudu_contract::tuple::tuple_datum::TupleDatumMarker; +use mudu_contract::database::sql_params::SQLParamMarker; + +// constant definition +const WALLETS:&str = "wallets"; + +const USER_ID:&str = "user_id"; + +const BALANCE:&str = "balance"; + +const UPDATED_AT:&str = "updated_at"; + + +// entity struct definition +#[derive(Debug, Clone, Default)] +pub struct Wallets { + + user_id: AttrUserId, + + balance: AttrBalance, + + updated_at: AttrUpdatedAt, + +} - updated_at: AttrUpdatedAt, +impl TupleDatumMarker for Wallets {} + +impl SQLParamMarker for Wallets {} + +impl Wallets { + pub fn new( + user_id: Option, + balance: Option, + updated_at: Option, + + ) -> Self { + let s = Self { + + user_id : AttrUserId::from(user_id), + + balance : AttrBalance::from(balance), + + updated_at : AttrUpdatedAt::from(updated_at), + + }; + s } - impl TupleDatumMarker for Wallets {} - - impl SQLParamMarker for Wallets {} - - impl Wallets { - pub fn new(user_id: Option, balance: Option, updated_at: Option) -> Self { - let s = Self { - user_id: AttrUserId::from(user_id), - - balance: AttrBalance::from(balance), - - updated_at: AttrUpdatedAt::from(updated_at), - }; - s - } - - pub fn new_empty() -> Self { - Self::default() - } + pub fn new_empty() -> Self { + Self::default() + } - pub fn set_user_id(&mut self, user_id: i32) { - self.user_id.update(user_id) - } + + pub fn set_user_id( + &mut self, + user_id: i32, + ) { + self.user_id.update(user_id) + } - pub fn get_user_id(&self) -> &Option { - self.user_id.get() - } + pub fn get_user_id( + &self, + ) -> &Option { + self.user_id.get() + } + + pub fn set_balance( + &mut self, + balance: i32, + ) { + self.balance.update(balance) + } - pub fn set_balance(&mut self, balance: i32) { - self.balance.update(balance) - } + pub fn get_balance( + &self, + ) -> &Option { + self.balance.get() + } + + pub fn set_updated_at( + &mut self, + updated_at: i32, + ) { + self.updated_at.update(updated_at) + } - pub fn get_balance(&self) -> &Option { - self.balance.get() - } + pub fn get_updated_at( + &self, + ) -> &Option { + self.updated_at.get() + } + +} - pub fn set_updated_at(&mut self, updated_at: i32) { - self.updated_at.update(updated_at) +impl Datum for Wallets { + fn dat_type() -> &'static DatType { + lazy_static! { + static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); } + &DAT_TYPE + } - pub fn get_updated_at(&self) -> &Option { - self.updated_at.get() - } + fn from_binary(binary: &[u8]) -> RS { + entity_utils::entity_from_binary(binary) } - impl Datum for Wallets { - fn dat_type() -> &'static DatType { - lazy_static! { - static ref DAT_TYPE: DatType = entity_utils::entity_dat_type::(); - } - &DAT_TYPE - } + fn from_value(value: &DatValue) -> RS { + entity_utils::entity_from_value(value) + } - fn from_binary(binary: &[u8]) -> RS { - entity_utils::entity_from_binary(binary) - } + fn from_textual(textual: &str) -> RS { + entity_utils::entity_from_textual(textual) + } +} - fn from_value(value: &DatValue) -> RS { - entity_utils::entity_from_value(value) - } +impl DatumDyn for Wallets { + fn dat_type_id(&self) -> RS { + entity_utils::entity_dat_type_id() + } - fn from_textual(textual: &str) -> RS { - entity_utils::entity_from_textual(textual) - } + fn to_binary(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_binary(self, dat_type) } - impl DatumDyn for Wallets { - fn dat_type_id(&self) -> RS { - entity_utils::entity_dat_type_id() - } + fn to_textual(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_textual(self, dat_type) + } - fn to_binary(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_binary(self, dat_type) - } + fn to_value(&self, dat_type: &DatType) -> RS { + entity_utils::entity_to_value(self, dat_type) + } - fn to_textual(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_textual(self, dat_type) - } + fn clone_boxed(&self) -> Box { + entity_utils::entity_clone_boxed(self) + } +} - fn to_value(&self, dat_type: &DatType) -> RS { - entity_utils::entity_to_value(self, dat_type) - } +impl Entity for Wallets { + fn new_empty() -> Self { + Self::new_empty() + } - fn clone_boxed(&self) -> Box { - entity_utils::entity_clone_boxed(self) - } + fn tuple_desc() -> &'static TupleFieldDesc { + lazy_static! { + static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ + + AttrUserId::datum_desc().clone(), + + AttrBalance::datum_desc().clone(), + + AttrUpdatedAt::datum_desc().clone(), + + ]); + } + &TUPLE_DESC } - impl Entity for Wallets { - fn new_empty() -> Self { - Self::new_empty() - } + fn object_name() -> &'static str { + WALLETS + } - fn tuple_desc() -> &'static TupleFieldDesc { - lazy_static! { - static ref TUPLE_DESC: TupleFieldDesc = TupleFieldDesc::new(vec![ - AttrUserId::datum_desc().clone(), - AttrBalance::datum_desc().clone(), - AttrUpdatedAt::datum_desc().clone(), - ]); + fn get_field_binary(&self, field: &str) -> RS>> { + match field { + + USER_ID => { + attr_field_access::attr_get_binary::<_>(self.user_id.get()) } - &TUPLE_DESC - } - - fn object_name() -> &'static str { - WALLETS + + BALANCE => { + attr_field_access::attr_get_binary::<_>(self.balance.get()) + } + + UPDATED_AT => { + attr_field_access::attr_get_binary::<_>(self.updated_at.get()) + } + + _ => { panic!("unknown name"); } } + } - fn get_field_binary(&self, field: &str) -> RS>> { - match field { - USER_ID => attr_field_access::attr_get_binary::<_>(self.user_id.get()), - - BALANCE => attr_field_access::attr_get_binary::<_>(self.balance.get()), - - UPDATED_AT => attr_field_access::attr_get_binary::<_>(self.updated_at.get()), - - _ => { - panic!("unknown name"); - } + fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { + match field { + + USER_ID => { + attr_field_access::attr_set_binary::<_, _>(self.user_id.get_mut(), binary.as_ref())?; + } + + BALANCE => { + attr_field_access::attr_set_binary::<_, _>(self.balance.get_mut(), binary.as_ref())?; } + + UPDATED_AT => { + attr_field_access::attr_set_binary::<_, _>(self.updated_at.get_mut(), binary.as_ref())?; + } + + _ => { panic!("unknown name"); } } + Ok(()) + } - fn set_field_binary>(&mut self, field: &str, binary: B) -> RS<()> { - match field { - USER_ID => { - attr_field_access::attr_set_binary::<_, _>( - self.user_id.get_mut(), - binary.as_ref(), - )?; - } - - BALANCE => { - attr_field_access::attr_set_binary::<_, _>( - self.balance.get_mut(), - binary.as_ref(), - )?; - } - - UPDATED_AT => { - attr_field_access::attr_set_binary::<_, _>( - self.updated_at.get_mut(), - binary.as_ref(), - )?; - } - - _ => { - panic!("unknown name"); - } + fn get_field_value(&self, field: &str) -> RS> { + match field { + + USER_ID => { + attr_field_access::attr_get_value::<_>(self.user_id.get()) } - Ok(()) + + BALANCE => { + attr_field_access::attr_get_value::<_>(self.balance.get()) + } + + UPDATED_AT => { + attr_field_access::attr_get_value::<_>(self.updated_at.get()) + } + + _ => { panic!("unknown name"); } } + } - fn get_field_value(&self, field: &str) -> RS> { - match field { - USER_ID => attr_field_access::attr_get_value::<_>(self.user_id.get()), - - BALANCE => attr_field_access::attr_get_value::<_>(self.balance.get()), - - UPDATED_AT => attr_field_access::attr_get_value::<_>(self.updated_at.get()), - - _ => { - panic!("unknown name"); - } + fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { + match field { + + USER_ID => { + attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; + } + + BALANCE => { + attr_field_access::attr_set_value::<_, _>(self.balance.get_mut(), value)?; + } + + UPDATED_AT => { + attr_field_access::attr_set_value::<_, _>(self.updated_at.get_mut(), value)?; } + + _ => { panic!("unknown name"); } } + Ok(()) + } +} - fn set_field_value>(&mut self, field: &str, value: B) -> RS<()> { - match field { - USER_ID => { - attr_field_access::attr_set_value::<_, _>(self.user_id.get_mut(), value)?; - } - - BALANCE => { - attr_field_access::attr_set_value::<_, _>(self.balance.get_mut(), value)?; - } - UPDATED_AT => { - attr_field_access::attr_set_value::<_, _>(self.updated_at.get_mut(), value)?; - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrUserId { + is_dirty:bool, + value: Option +} - _ => { - panic!("unknown name"); - } - } - Ok(()) +impl AttrUserId { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrUserId { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrUserId { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrUserId { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrUserId { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + WALLETS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + USER_ID + } +} - fn object_name() -> &'static str { - WALLETS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrBalance { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - USER_ID +impl AttrBalance { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrBalance { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrBalance { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrBalance { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrBalance { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + WALLETS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + BALANCE + } +} - fn object_name() -> &'static str { - WALLETS - } +// attribute struct definition +#[derive(Default, Clone, Debug)] +pub struct AttrUpdatedAt { + is_dirty:bool, + value: Option +} - fn attr_name() -> &'static str { - BALANCE +impl AttrUpdatedAt { + fn from(value:Option) -> Self { + Self { + is_dirty: false, + value } } - // attribute struct definition - #[derive(Default, Clone, Debug)] - pub struct AttrUpdatedAt { - is_dirty: bool, - value: Option, + fn get(&self) -> &Option { + &self.value } - impl AttrUpdatedAt { - fn from(value: Option) -> Self { - Self { - is_dirty: false, - value, - } - } + fn get_mut(&mut self) -> &mut Option { + &mut self.value + } - fn get(&self) -> &Option { - &self.value - } + fn set(&mut self, value:Option) { + self.value = value + } - fn get_mut(&mut self) -> &mut Option { - &mut self.value - } + fn update(&mut self, value: i32) { + self.is_dirty = true; + self.value = Some(value) + } +} - fn set(&mut self, value: Option) { - self.value = value - } +impl AttrValue for AttrUpdatedAt { + fn dat_type() -> &'static DatType { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) + } - fn update(&mut self, value: i32) { - self.is_dirty = true; - self.value = Some(value) - } + fn datum_desc() -> &'static DatumDesc { + static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); + ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) } - impl AttrValue for AttrUpdatedAt { - fn dat_type() -> &'static DatType { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_dat_type()) - } + fn object_name() -> &'static str { + WALLETS + } - fn datum_desc() -> &'static DatumDesc { - static ONCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); - ONCE_LOCK.get_or_init(|| Self::attr_datum_desc()) - } + fn attr_name() -> &'static str { + UPDATED_AT + } +} - fn object_name() -> &'static str { - WALLETS - } - fn attr_name() -> &'static str { - UPDATED_AT - } - } } diff --git a/example/wallet/src/rust/warehouse.rs b/example/wallet/src/rust/warehouse.rs index 9544dd2..237ecda 100644 --- a/example/wallet/src/rust/warehouse.rs +++ b/example/wallet/src/rust/warehouse.rs @@ -158,7 +158,7 @@ pub mod object { } } - impl DatumDyn for Warehouse { +impl DatumDyn for Warehouse { fn dat_type_id(&self) -> RS { entity_utils::entity_dat_type_id() } @@ -175,10 +175,44 @@ pub mod object { entity_utils::entity_to_value(self, dat_type) } - fn clone_boxed(&self) -> Box { - entity_utils::entity_clone_boxed(self) - } + fn clone_boxed(&self) -> Box { + entity_utils::entity_clone_boxed(self) + } +} + +#[cfg(test)] +mod tests { + use super::Warehouse; + use mudu_type::datum::{Datum, DatumDyn}; + + #[test] + fn warehouse_roundtrip_and_setters_work() { + let mut warehouse = Warehouse::new( + Some(1), + Some(10.5), + Some(0.1), + Some("Main".to_string()), + Some("Street1".to_string()), + Some("Street2".to_string()), + Some("City".to_string()), + Some("ST".to_string()), + Some("10000".to_string()), + ); + + warehouse.set_w_name("Central".to_string()); + warehouse.set_w_tax(0.2); + assert_eq!(warehouse.get_w_name().as_deref(), Some("Central")); + assert_eq!(warehouse.get_w_tax(), &Some(0.2)); + + let from_value = Warehouse::from_value(&warehouse.to_value(Warehouse::dat_type()).unwrap()).unwrap(); + assert_eq!(from_value.get_w_city().as_deref(), Some("City")); + + let from_binary = + Warehouse::from_binary(warehouse.to_binary(Warehouse::dat_type()).unwrap().as_ref()) + .unwrap(); + assert_eq!(from_binary.get_w_zip().as_deref(), Some("10000")); } +} impl Entity for Warehouse { fn new_empty() -> Self { diff --git a/example/ycsb/Cargo.toml b/example/ycsb/Cargo.toml index 9907fd6..1b50880 100644 --- a/example/ycsb/Cargo.toml +++ b/example/ycsb/Cargo.toml @@ -36,6 +36,11 @@ clap = { workspace = true, optional = true, features = ["derive"] } tokio = { workspace = true, optional = true } async-backtrace = { workspace = true, optional = true } +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +mudu_adapter = { workspace = true } +sys_interface = { workspace = true, features = ["async", "standalone-adapter"] } +tokio = { workspace = true, features = ["rt-multi-thread"] } + [[bin]] name = "ycsb-benchmark" path = "src/bin/ycsb_benchmark.rs" diff --git a/example/ycsb/README.md b/example/ycsb/README.md index 007cdb1..0542a44 100644 --- a/example/ycsb/README.md +++ b/example/ycsb/README.md @@ -67,7 +67,7 @@ cargo run -p ycsb --features benchmark-runner --bin ycsb-benchmark -- \ Example with mudud TCP: ```bash -export MUDU_CONNECTION="mudud://127.0.0.1:9000/ycsb?http_addr=127.0.0.1:8300" +export MUDU_CONNECTION="mudud://127.0.0.1:9527/ycsb?http_addr=127.0.0.1:8300" cargo run -p ycsb --features benchmark-runner --bin ycsb-benchmark -- \ --workload f \ --enable-async \ diff --git a/example/ycsb/src/bin/ycsb_benchmark.rs b/example/ycsb/src/bin/ycsb_benchmark.rs index c8e9655..42ed1d0 100644 --- a/example/ycsb/src/bin/ycsb_benchmark.rs +++ b/example/ycsb/src/bin/ycsb_benchmark.rs @@ -697,9 +697,13 @@ fn load_partition_routing_sync(partition_count: usize) -> RS { e ) })?; - let topology = runtime - .block_on(fetch_server_topology(&http_addr)) - .map_err(|e| mudu::m_error!(mudu::error::ec::EC::NetErr, e))?; + let topology = match runtime.block_on(fetch_server_topology(&http_addr)) { + Ok(topology) => topology, + Err(err) if topology_is_unsupported(&err) => { + return Ok(default_partition_routing(partition_count)); + } + Err(err) => return Err(mudu::m_error!(mudu::error::ec::EC::NetErr, err)), + }; build_partition_routing(partition_count, topology) } @@ -707,12 +711,21 @@ async fn load_partition_routing_async(partition_count: usize) -> RS topology, + Err(err) if topology_is_unsupported(&err) => { + return Ok(default_partition_routing(partition_count)); + } + Err(err) => return Err(mudu::m_error!(mudu::error::ec::EC::NetErr, err)), + }; build_partition_routing(partition_count, topology) } +fn topology_is_unsupported(err: &str) -> bool { + err.contains("server topology is not supported") + || err.contains("\"code\":\"NotImplemented\"") +} + fn default_partition_routing(partition_count: usize) -> PartitionRouting { PartitionRouting { slots: (0..partition_count) diff --git a/example/ycsb/src/lib.rs b/example/ycsb/src/lib.rs index df760f2..935ee1d 100644 --- a/example/ycsb/src/lib.rs +++ b/example/ycsb/src/lib.rs @@ -5,3 +5,9 @@ pub mod rust; #[allow(unused)] #[cfg(target_arch = "wasm32")] pub mod generated; + +#[cfg(test)] +pub(crate) fn test_lock() -> &'static std::sync::Mutex<()> { + static LOCK: std::sync::OnceLock> = std::sync::OnceLock::new(); + LOCK.get_or_init(|| std::sync::Mutex::new(())) +} diff --git a/example/ycsb/src/rust/procedure.rs b/example/ycsb/src/rust/procedure.rs index d578f07..e58c757 100644 --- a/example/ycsb/src/rust/procedure.rs +++ b/example/ycsb/src/rust/procedure.rs @@ -56,3 +56,49 @@ pub fn ycsb_read_modify_write(xid: XID, user_key: String, append_value: String) mudu_put(xid, key.as_bytes(), current.as_bytes())?; Ok(current) } + +#[cfg(test)] +mod tests { + use super::{ycsb_insert, ycsb_read, ycsb_read_modify_write, ycsb_scan, ycsb_update}; + use crate::test_lock; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + use sys_interface::sync_api::{mudu_close, mudu_open}; + + fn temp_db_path(name: &str) -> PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("ycsb_{name}_{suffix}.db")) + } + + #[test] + fn ycsb_sync_procedures_roundtrip_against_standalone_adapter() { + let _guard = test_lock().lock().unwrap_or_else(|err| err.into_inner()); + let db_path = temp_db_path("sync"); + mudu_adapter::config::reset_db_path_override_for_test(); + mudu_adapter::syscall::set_db_path(&db_path); + + let xid = mudu_open().unwrap(); + ycsb_insert(xid, "u1".to_string(), "v1".to_string()).unwrap(); + ycsb_insert(xid, "u2".to_string(), "v2".to_string()).unwrap(); + + assert_eq!(ycsb_read(xid, "u1".to_string()).unwrap(), "v1"); + + ycsb_update(xid, "u1".to_string(), "v3".to_string()).unwrap(); + assert_eq!(ycsb_read(xid, "u1".to_string()).unwrap(), "v3"); + + assert_eq!( + ycsb_scan(xid, "u1".to_string(), "uz".to_string()).unwrap(), + vec!["user/u1=v3".to_string(), "user/u2=v2".to_string()] + ); + + assert_eq!( + ycsb_read_modify_write(xid, "u1".to_string(), "-x".to_string()).unwrap(), + "v3-x" + ); + + mudu_close(xid).unwrap(); + } +} diff --git a/example/ycsb/src/rust/procedure_async.rs b/example/ycsb/src/rust/procedure_async.rs index 54d2654..1abf76d 100644 --- a/example/ycsb/src/rust/procedure_async.rs +++ b/example/ycsb/src/rust/procedure_async.rs @@ -72,3 +72,60 @@ pub async fn ycsb_read_modify_write( mudu_put(xid, key.as_bytes(), current.as_bytes()).await?; Ok(current) } + +#[cfg(test)] +mod tests { + use super::{ycsb_insert, ycsb_read, ycsb_read_modify_write, ycsb_scan, ycsb_update}; + use crate::test_lock; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + use sys_interface::async_api::{mudu_close, mudu_open}; + + fn temp_db_path(name: &str) -> PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("ycsb_async_{name}_{suffix}.db")) + } + + #[test] + fn ycsb_async_procedures_roundtrip_against_standalone_adapter() { + let _guard = test_lock().lock().unwrap_or_else(|err| err.into_inner()); + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + let db_path = temp_db_path("async"); + mudu_adapter::config::reset_db_path_override_for_test(); + mudu_adapter::syscall::set_db_path(&db_path); + + let xid = mudu_open().await.unwrap(); + ycsb_insert(xid, "u1".to_string(), "v1".to_string()) + .await + .unwrap(); + ycsb_insert(xid, "u2".to_string(), "v2".to_string()) + .await + .unwrap(); + + assert_eq!(ycsb_read(xid, "u1".to_string()).await.unwrap(), "v1"); + + ycsb_update(xid, "u1".to_string(), "v3".to_string()) + .await + .unwrap(); + assert_eq!(ycsb_read(xid, "u1".to_string()).await.unwrap(), "v3"); + + assert_eq!( + ycsb_scan(xid, "u1".to_string(), "uz".to_string()).await.unwrap(), + vec!["user/u1=v3".to_string(), "user/u2=v2".to_string()] + ); + + assert_eq!( + ycsb_read_modify_write(xid, "u1".to_string(), "-x".to_string()) + .await + .unwrap(), + "v3-x" + ); + + mudu_close(xid).await.unwrap(); + }); + } +} diff --git a/mudu/src/common/len_payload.rs b/mudu/src/common/len_payload.rs index ef3502a..01802bf 100644 --- a/mudu/src/common/len_payload.rs +++ b/mudu/src/common/len_payload.rs @@ -20,3 +20,18 @@ impl LenPayload { Endian::write_u32(s, len); } } + +#[cfg(test)] +mod tests { + use super::LenPayload; + + #[test] + fn len_payload_reads_and_writes_header() { + let mut buf = vec![0_u8; 7]; + LenPayload::set_len(&mut buf, 3); + buf[4..].copy_from_slice(b"xyz"); + + assert_eq!(LenPayload::len(&buf), 3); + assert_eq!(LenPayload::payload(&buf), b"xyz"); + } +} diff --git a/mudu/src/common/slice.rs b/mudu/src/common/slice.rs index db6147a..24e76af 100644 --- a/mudu/src/common/slice.rs +++ b/mudu/src/common/slice.rs @@ -71,3 +71,51 @@ impl Decoder for SliceRef<'_> { } } } + +#[cfg(test)] +mod tests { + use super::{SliceMutRef, SliceRef}; + use crate::common::codec::{Decoder, Encoder}; + + #[test] + fn slice_mut_ref_tracks_written_bytes() { + let mut buf = [0_u8; 6]; + let mut writer = SliceMutRef::new(&mut buf); + + writer.write(b"ab").unwrap(); + writer.write(b"cd").unwrap(); + + assert_eq!(writer.capacity(), 6); + assert_eq!(writer.write_pos(), 4); + assert_eq!(writer.as_slice(), b"abcd"); + } + + #[test] + fn slice_ref_reads_incrementally_and_resize_resets_cursor() { + let mut reader = SliceRef::new(b"wxyz"); + let mut buf = [0_u8; 2]; + + reader.read(&mut buf).unwrap(); + assert_eq!(&buf, b"wx"); + assert_eq!(reader.read_pos(), 2); + assert_eq!(reader.as_slice(), b"wx"); + + reader.resize(); + assert_eq!(reader.read_pos(), 0); + assert_eq!(reader.as_slice(), b""); + } + + #[test] + fn slice_mut_ref_write_returns_capacity_error() { + let mut buf = [0_u8; 2]; + let mut writer = SliceMutRef::new(&mut buf); + assert!(writer.write(b"abc").is_err()); + } + + #[test] + fn slice_ref_read_returns_capacity_error() { + let mut reader = SliceRef::new(b"a"); + let mut buf = [0_u8; 2]; + assert!(reader.read(&mut buf).is_err()); + } +} diff --git a/mudu/src/common/update_delta.rs b/mudu/src/common/update_delta.rs index e8e8114..d8fabac 100644 --- a/mudu/src/common/update_delta.rs +++ b/mudu/src/common/update_delta.rs @@ -1,5 +1,5 @@ -use crate::common::codec::{DecErr, Decode, Decoder, EncErr, Encode, Encoder}; use crate::common::buf::Buf; +use crate::common::codec::{DecErr, Decode, Decoder, EncErr, Encode, Encoder}; #[cfg(any(test, feature = "test"))] use arbitrary::{Arbitrary, Unstructured}; use std::cell::RefCell; diff --git a/mudu/src/utils/buf.rs b/mudu/src/utils/buf.rs index 30334c9..cbd8559 100644 --- a/mudu/src/utils/buf.rs +++ b/mudu/src/utils/buf.rs @@ -17,7 +17,7 @@ pub fn read_sized_buf(buf: &[u8]) -> Result<(u32, &[u8]), Option> { return Err(None); } let n = read_u32(buf); - if n as usize + len_bytes < buf.len() { + if buf.len() < n as usize + len_bytes { return Err(Some(n)); } Ok(( diff --git a/mudu/src/utils/json.rs b/mudu/src/utils/json.rs index 35dd3dd..06b5b4e 100644 --- a/mudu/src/utils/json.rs +++ b/mudu/src/utils/json.rs @@ -61,3 +61,55 @@ pub fn write_json>(object: &S, path: P) -> RS<()> { })?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::{ + from_json_str, from_json_value, read_json, to_json_str, to_json_value, write_json, + JsonValue, + }; + use serde::{Deserialize, Serialize}; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct DemoJson { + id: u32, + name: String, + } + + fn temp_path(name: &str) -> std::path::PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("mudu_json_{name}_{suffix}.json")) + } + + #[test] + fn json_string_value_and_file_roundtrip() { + let value = DemoJson { + id: 9, + name: "neo".to_string(), + }; + + let json = to_json_str(&value).unwrap(); + assert!(json.contains("\"name\"")); + let decoded: DemoJson = from_json_str(&json).unwrap(); + assert_eq!(decoded, value); + + let json_value = to_json_value(&value).unwrap(); + let decoded_from_value: DemoJson = from_json_value(json_value).unwrap(); + assert_eq!(decoded_from_value, value); + + let path = temp_path("roundtrip"); + write_json(&value, &path).unwrap(); + let loaded: DemoJson = read_json(&path).unwrap(); + assert_eq!(loaded, value); + } + + #[test] + fn json_decode_rejects_wrong_shape() { + let err = from_json_value::(JsonValue::String("oops".to_string())).unwrap_err(); + assert!(err.to_string().contains("DecodeErr")); + } +} diff --git a/mudu/src/utils/msg_pack.rs b/mudu/src/utils/msg_pack.rs index 753f408..18efc2b 100644 --- a/mudu/src/utils/msg_pack.rs +++ b/mudu/src/utils/msg_pack.rs @@ -9,10 +9,11 @@ pub type MsgPackValue = rmpv::Value; pub type MsgPackUtf8String = rmpv::Utf8String; pub fn msg_pack_value_to_binary(value: &MsgPackValue) -> RS> { - let mut sizer = Sizer::new(); - rmpv::encode::write_value(&mut sizer, value).unwrap(); - let mut vec = Vec::with_capacity(sizer.size()); - vec.resize(sizer.size(), 0u8); + let mut vec = Vec::with_capacity({ + let mut sizer = Sizer::new(); + rmpv::encode::write_value(&mut sizer, value).unwrap(); + sizer.size() + }); rmpv::encode::write_value(&mut vec, value).unwrap(); Ok(vec) } @@ -23,3 +24,27 @@ pub fn msg_pack_binary_to_value(binary: &[u8]) -> RS<(MsgPackValue, u64)> { .map_err(|e| m_error!(EC::DecodeErr, "cannot decode from msg pack binary", e))?; Ok((v, cursor.position())) } + +#[cfg(test)] +mod tests { + use super::{msg_pack_binary_to_value, msg_pack_value_to_binary, MsgPackValue}; + + #[test] + fn msg_pack_roundtrip_preserves_value_and_position() { + let value = MsgPackValue::Array(vec![ + MsgPackValue::from(7), + MsgPackValue::from("neo"), + ]); + + let binary = msg_pack_value_to_binary(&value).unwrap(); + let (decoded, used) = msg_pack_binary_to_value(&binary).unwrap(); + + assert_eq!(decoded, value); + assert_eq!(used as usize, binary.len()); + } + + #[test] + fn msg_pack_decode_invalid_binary_returns_error() { + assert!(msg_pack_binary_to_value(&[0x92, 0x01]).is_err()); + } +} diff --git a/mudu/src/utils/toml.rs b/mudu/src/utils/toml.rs index ae6073c..95f7ec5 100644 --- a/mudu/src/utils/toml.rs +++ b/mudu/src/utils/toml.rs @@ -36,3 +36,47 @@ pub fn read_toml>(path: P) -> RS { .map_err(|e| m_error!(EC::DecodeErr, "decode from toml string error", e))?; Ok(ret) } + +#[cfg(test)] +mod tests { + use super::{read_toml, to_toml_str, write_toml}; + use serde::{Deserialize, Serialize}; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct DemoToml { + id: u32, + name: String, + } + + fn temp_path(name: &str) -> std::path::PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("mudu_toml_{name}_{suffix}.toml")) + } + + #[test] + fn toml_string_and_file_roundtrip() { + let value = DemoToml { + id: 7, + name: "alice".to_string(), + }; + let toml = to_toml_str(&value).unwrap(); + assert!(toml.contains("id = 7")); + + let path = temp_path("roundtrip"); + write_toml(&value, &path).unwrap(); + let loaded: DemoToml = read_toml(&path).unwrap(); + assert_eq!(loaded, value); + } + + #[test] + fn read_toml_rejects_invalid_input() { + let path = temp_path("invalid"); + std::fs::write(&path, "not = [valid").unwrap(); + let err = read_toml::(&path).unwrap_err(); + assert!(err.to_string().contains("DecodeErr")); + } +} diff --git a/mudu_adapter/src/backend.rs b/mudu_adapter/src/backend.rs index 7747817..fabe8f3 100644 --- a/mudu_adapter/src/backend.rs +++ b/mudu_adapter/src/backend.rs @@ -159,10 +159,9 @@ pub fn mudu_command(oid: OID, sql_stmt: &dyn SQLStmt, params: &dyn SQLParams) -> pub fn mudu_batch(oid: OID, sql_stmt: &dyn SQLStmt, params: &dyn SQLParams) -> RS { match config::driver() { Driver::Sqlite => sqlite::mudu_batch(oid, sql_stmt, params), - Driver::Postgres | Driver::MySql | Driver::Mudud => Err(mudu::m_error!( - mudu::error::ec::EC::NotImplemented, - "batch syscall is only implemented for sqlite standalone adapter" - )), + Driver::Postgres => postgres::mudu_batch(oid, sql_stmt, params), + Driver::MySql => mysql::mudu_batch(oid, sql_stmt, params), + Driver::Mudud => mududb::mudu_batch(oid, sql_stmt, params), } } @@ -183,10 +182,9 @@ pub async fn mudu_command_async( pub async fn mudu_batch_async(oid: OID, sql_stmt: &dyn SQLStmt, params: &dyn SQLParams) -> RS { match config::driver() { Driver::Sqlite => sqlite::mudu_batch_async(oid, sql_stmt, params).await, - Driver::Postgres | Driver::MySql | Driver::Mudud => Err(mudu::m_error!( - mudu::error::ec::EC::NotImplemented, - "batch syscall is only implemented for sqlite standalone adapter" - )), + Driver::Postgres => postgres::mudu_batch_async(oid, sql_stmt, params).await, + Driver::MySql => mysql::mudu_batch_async(oid, sql_stmt, params).await, + Driver::Mudud => mududb::mudu_batch_async(oid, sql_stmt, params).await, } } diff --git a/mudu_adapter/src/config.rs b/mudu_adapter/src/config.rs index cd6b468..a89b58f 100644 --- a/mudu_adapter/src/config.rs +++ b/mudu_adapter/src/config.rs @@ -35,6 +35,13 @@ pub fn set_db_path(path: impl Into) { *lock.write().expect("db path lock poisoned") = Some(path.into()); } +#[doc(hidden)] +pub fn reset_db_path_override_for_test() { + if let Some(lock) = DB_PATH_OVERRIDE.get() { + *lock.write().expect("db path lock poisoned") = None; + } +} + pub fn db_path() -> PathBuf { if let Some(lock) = DB_PATH_OVERRIDE.get() { if let Some(path) = lock.read().expect("db path lock poisoned").clone() { diff --git a/mudu_adapter/src/mududb.rs b/mudu_adapter/src/mududb.rs index c6d7ed4..2ed027a 100644 --- a/mudu_adapter/src/mududb.rs +++ b/mudu_adapter/src/mududb.rs @@ -1,6 +1,6 @@ use crate::config; use crate::result_set::LocalResultSet; -use crate::sql::{datum_type_for_id, replace_placeholders}; +use crate::sql::replace_placeholders; use crate::state; use lazy_static::lazy_static; use mudu::common::id::OID; @@ -19,11 +19,8 @@ use mudu_contract::protocol::{ ClientRequest, GetRequest, PutRequest, RangeScanRequest, SessionCloseRequest, SessionCreateRequest, }; -use mudu_contract::tuple::datum_desc::DatumDesc; use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; use mudu_contract::tuple::tuple_value::TupleValue; -use mudu_type::dat_type_id::DatTypeID; -use mudu_type::dat_value::DatValue; use scc::HashMap as SccHashMap; use std::collections::HashMap; use std::sync::mpsc::{self, Receiver, Sender, SyncSender}; @@ -51,8 +48,8 @@ struct AsyncMududSession { } struct QueryRows { - columns: Vec, - rows: Vec>, + row_desc: TupleFieldDesc, + rows: Vec, } enum AsyncCommand { @@ -94,6 +91,12 @@ enum AsyncCommand { sql_text: String, response: SyncSender>, }, + Batch { + session_id: OID, + app_name: String, + sql_text: String, + response: SyncSender>, + }, } struct AsyncManager { @@ -305,12 +308,8 @@ pub fn mudu_query( with_session(session_id, |session| { let response = session.client.query(app_name.clone(), sql_text.clone())?; - let desc = build_desc(response.columns()); - let rows = response - .rows() - .iter() - .map(|row| row_to_tuple_value(row)) - .collect::>>()?; + let desc = response.row_desc().clone(); + let rows = response.rows().to_vec(); Ok(RecordSet::new( Arc::new(LocalResultSet::new(rows)), Arc::new(desc), @@ -332,12 +331,8 @@ pub async fn mudu_query_async( .client .query(ClientRequest::new(&app_name, &sql_text)) .await?; - let desc = build_desc(response.columns()); - let rows = response - .rows() - .iter() - .map(|row| row_to_tuple_value(row)) - .collect::>>()?; + let desc = response.row_desc().clone(); + let rows = response.rows().to_vec(); Ok(RecordSet::new( Arc::new(LocalResultSet::new(rows)), Arc::new(desc), @@ -359,6 +354,27 @@ pub fn mudu_command(session_id: OID, sql_stmt: &dyn SQLStmt, params: &dyn SQLPar }) } +pub fn mudu_batch(session_id: OID, sql_stmt: &dyn SQLStmt, params: &dyn SQLParams) -> RS { + if params.size() != 0 { + return Err(m_error!( + EC::NotImplemented, + "batch syscall does not support SQL parameters" + )); + } + let app_name = config::mudud_app_name() + .ok_or_else(|| m_error!(EC::DBInternalError, "missing mudud app name"))?; + let sql_text = sql_stmt.to_sql_string(); + + if config::mudud_async_session_loop() { + return async_batch(session_id, app_name, sql_text); + } + + with_session(session_id, |session| { + let response = session.client.batch(app_name.clone(), sql_text.clone())?; + Ok(response.affected_rows()) + }) +} + pub async fn mudu_command_async( session_id: OID, sql_stmt: &dyn SQLStmt, @@ -377,6 +393,28 @@ pub async fn mudu_command_async( Ok(response.affected_rows()) } +pub async fn mudu_batch_async( + session_id: OID, + sql_stmt: &dyn SQLStmt, + params: &dyn SQLParams, +) -> RS { + if params.size() != 0 { + return Err(m_error!( + EC::NotImplemented, + "batch syscall does not support SQL parameters" + )); + } + let app_name = config::mudud_app_name() + .ok_or_else(|| m_error!(EC::DBInternalError, "missing mudud app name"))?; + let session = async_session(session_id).await?; + let mut session = session.lock().await; + let response = session + .client + .batch(ClientRequest::new(&app_name, sql_stmt.to_sql_string())) + .await?; + Ok(response.affected_rows()) +} + fn with_session(session_id: OID, f: F) -> RS where F: FnOnce(&mut MududSession) -> RS, @@ -408,23 +446,6 @@ async fn async_session(session_id: OID) -> RS> }) } -fn build_desc(columns: &[String]) -> TupleFieldDesc { - let fields = columns - .iter() - .map(|column| DatumDesc::new(column.clone(), datum_type_for_id(DatTypeID::String))) - .collect(); - TupleFieldDesc::new(fields) -} - -fn row_to_tuple_value(row: &[String]) -> RS { - Ok(TupleValue::from( - row.iter() - .cloned() - .map(DatValue::from_string) - .collect::>(), - )) -} - fn async_open(worker_id: OID) -> RS { let session_id = state::next_session_id(); let (tx, rx) = mpsc::sync_channel(1); @@ -505,12 +526,8 @@ fn async_query(session_id: OID, app_name: String, sql_text: String) - }) .map_err(|e| m_error!(EC::ThreadErr, "send mudud async query command error", e))?; let response = recv_response(rx)?; - let desc = build_desc(&response.columns); - let rows = response - .rows - .iter() - .map(|row| row_to_tuple_value(row)) - .collect::>>()?; + let desc = response.row_desc; + let rows = response.rows; Ok(RecordSet::new( Arc::new(LocalResultSet::new(rows)), Arc::new(desc), @@ -531,6 +548,20 @@ fn async_command(session_id: OID, app_name: String, sql_text: String) -> RS recv_response(rx) } +fn async_batch(session_id: OID, app_name: String, sql_text: String) -> RS { + let (tx, rx) = mpsc::sync_channel(1); + ASYNC_MANAGER + .sender + .send(AsyncCommand::Batch { + session_id, + app_name, + sql_text, + response: tx, + }) + .map_err(|e| m_error!(EC::ThreadErr, "send mudud async batch command error", e))?; + recv_response(rx) +} + fn recv_response(rx: Receiver>) -> RS { rx.recv() .map_err(|e| m_error!(EC::ThreadErr, "receive mudud async response error", e))? @@ -723,7 +754,7 @@ async fn handle_async_command( .query(ClientRequest::new(app_name, sql_text)) .await?; Ok(QueryRows { - columns: response_data.columns().to_vec(), + row_desc: response_data.row_desc().clone(), rows: response_data.rows().to_vec(), }) } @@ -752,5 +783,27 @@ async fn handle_async_command( .await; let _ = response.send(result); } + AsyncCommand::Batch { + session_id, + app_name, + sql_text, + response, + } => { + let result = async { + let session = sessions.get_mut(&session_id).ok_or_else(|| { + m_error!( + EC::NoSuchElement, + format!("session {} does not exist", session_id) + ) + })?; + let response_data = session + .client + .batch(ClientRequest::new(app_name, sql_text)) + .await?; + Ok(response_data.affected_rows()) + } + .await; + let _ = response.send(result); + } } } diff --git a/mudu_adapter/src/mysql.rs b/mudu_adapter/src/mysql.rs index e3fb90f..a818bc4 100644 --- a/mudu_adapter/src/mysql.rs +++ b/mudu_adapter/src/mysql.rs @@ -295,6 +295,20 @@ pub fn mudu_command(oid: OID, sql_stmt: &dyn SQLStmt, params: &dyn SQLParams) -> }) } +pub fn mudu_batch(oid: OID, sql_stmt: &dyn SQLStmt, params: &dyn SQLParams) -> RS { + if params.size() != 0 { + return Err(m_error!( + EC::NotImplemented, + "batch syscall does not support SQL parameters" + )); + } + with_session(oid, |conn| { + conn.query_drop(sql_stmt.to_sql_string()) + .map_err(|e| m_error!(EC::DBInternalError, "mysql batch error", e))?; + Ok(conn.affected_rows()) + }) +} + pub async fn mudu_command_async( oid: OID, sql_stmt: &dyn SQLStmt, @@ -312,6 +326,27 @@ pub async fn mudu_command_async( Ok(session.conn.affected_rows()) } +pub async fn mudu_batch_async( + oid: OID, + sql_stmt: &dyn SQLStmt, + params: &dyn SQLParams, +) -> RS { + if params.size() != 0 { + return Err(m_error!( + EC::NotImplemented, + "batch syscall does not support SQL parameters" + )); + } + let session = with_async_session(oid).await?; + let mut session = session.lock().await; + session + .conn + .query_drop(sql_stmt.to_sql_string()) + .await + .map_err(|e| m_error!(EC::DBInternalError, "mysql batch error", e))?; + Ok(session.conn.affected_rows()) +} + fn ensure_session_exists(session_id: OID) -> RS<()> { if SESSIONS.contains_sync(&session_id) { Ok(()) diff --git a/mudu_adapter/src/postgres.rs b/mudu_adapter/src/postgres.rs index d9e34e5..1dac871 100644 --- a/mudu_adapter/src/postgres.rs +++ b/mudu_adapter/src/postgres.rs @@ -329,6 +329,21 @@ pub fn mudu_command(oid: OID, sql_stmt: &dyn SQLStmt, params: &dyn SQLParams) -> }) } +pub fn mudu_batch(oid: OID, sql_stmt: &dyn SQLStmt, params: &dyn SQLParams) -> RS { + if params.size() != 0 { + return Err(m_error!( + EC::NotImplemented, + "batch syscall does not support SQL parameters" + )); + } + with_session(oid, |client| { + client + .batch_execute(&sql_stmt.to_sql_string()) + .map_err(|e| m_error!(EC::DBInternalError, "execute postgres batch error", e))?; + Ok(0) + }) +} + pub async fn mudu_command_async( oid: OID, sql_stmt: &dyn SQLStmt, @@ -344,6 +359,26 @@ pub async fn mudu_command_async( .map_err(|e| m_error!(EC::DBInternalError, "postgres command error", e)) } +pub async fn mudu_batch_async( + oid: OID, + sql_stmt: &dyn SQLStmt, + params: &dyn SQLParams, +) -> RS { + if params.size() != 0 { + return Err(m_error!( + EC::NotImplemented, + "batch syscall does not support SQL parameters" + )); + } + let session = with_async_session(oid).await?; + session + .client + .batch_execute(&sql_stmt.to_sql_string()) + .await + .map_err(|e| m_error!(EC::DBInternalError, "execute postgres batch error", e))?; + Ok(0) +} + fn ensure_session_exists(session_id: OID) -> RS<()> { if SESSIONS.contains_sync(&session_id) { Ok(()) diff --git a/mudu_adapter/tests/adapter_coverage_test.rs b/mudu_adapter/tests/adapter_coverage_test.rs new file mode 100644 index 0000000..7313b3a --- /dev/null +++ b/mudu_adapter/tests/adapter_coverage_test.rs @@ -0,0 +1,197 @@ +use mudu_adapter::{backend, config, kv, sqlite}; +use mudu_contract::database::sql_stmt_text::SQLStmtText; +use std::env; +use std::path::PathBuf; +use std::sync::{Mutex, OnceLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn test_lock() -> &'static Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) +} + +fn temp_db_path(name: &str) -> PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "mudu_adapter_{name}_{suffix}.db" + )) +} + +fn with_connection_env(value: &str, f: impl FnOnce() -> T) -> T { + let prev = env::var("MUDU_CONNECTION").ok(); + // SAFETY: tests serialize access through test_lock(), so process env mutation is not concurrent here. + unsafe { env::set_var("MUDU_CONNECTION", value) }; + let result = f(); + match prev { + Some(prev) => { + // SAFETY: guarded by the same test mutex. + unsafe { env::set_var("MUDU_CONNECTION", prev) }; + } + None => { + // SAFETY: guarded by the same test mutex. + unsafe { env::remove_var("MUDU_CONNECTION") }; + } + } + result +} + +#[test] +fn connection_parses_supported_driver_variants() { + let _guard = test_lock().lock().unwrap(); + config::reset_db_path_override_for_test(); + + with_connection_env("postgres://user:pw@localhost/db", || { + assert_eq!(config::driver(), config::Driver::Postgres); + assert_eq!( + config::postgres_url().as_deref(), + Some("postgres://user:pw@localhost/db") + ); + }); + + with_connection_env("mysql://user:pw@localhost/db", || { + assert_eq!(config::driver(), config::Driver::MySql); + assert_eq!( + config::mysql_url().as_deref(), + Some("mysql://user:pw@localhost/db") + ); + }); + + with_connection_env( + "mudud://127.0.0.1:9527/demo?http_addr=127.0.0.1:8301&async=true", + || { + assert_eq!(config::driver(), config::Driver::Mudud); + assert_eq!(config::mudud_addr().as_deref(), Some("127.0.0.1:9527")); + assert_eq!(config::mudud_http_addr().as_deref(), Some("127.0.0.1:8301")); + assert_eq!(config::mudud_app_name().as_deref(), Some("demo")); + assert!(config::mudud_async_session_loop()); + }, + ); + + with_connection_env("sqlite://./adapter_test.db", || { + assert_eq!(config::driver(), config::Driver::Sqlite); + assert!( + config::db_path() + .to_string_lossy() + .ends_with("adapter_test.db") + ); + }); +} + +#[test] +fn replace_placeholders_formats_supported_sqlite_values() { + let _guard = test_lock().lock().unwrap(); + let sql = "INSERT INTO demo VALUES (?, ?, ?, ?)"; + let params = (7_i32, 9_i64, 1.5_f32, String::from("abc")); + let rendered = backend::replace_placeholders(sql, ¶ms).unwrap(); + assert_eq!(rendered, "INSERT INTO demo VALUES (7, 9, 1.5, \"abc\")"); +} + +#[test] +fn sqlite_session_kv_and_batch_flow_work_end_to_end() { + let _guard = test_lock().lock().unwrap(); + config::reset_db_path_override_for_test(); + let db_path = temp_db_path("sqlite_kv"); + config::set_db_path(&db_path); + + let session_id = sqlite::mudu_open().unwrap(); + kv::put(session_id, b"k2", b"v2").unwrap(); + kv::put(session_id, b"k1", b"v1").unwrap(); + kv::put(session_id, b"k3", b"v3").unwrap(); + + assert_eq!(kv::get(session_id, b"k2").unwrap(), Some(b"v2".to_vec())); + assert_eq!( + kv::range(session_id, b"k1", b"k3").unwrap(), + vec![ + (b"k1".to_vec(), b"v1".to_vec()), + (b"k2".to_vec(), b"v2".to_vec()), + ] + ); + assert_eq!( + kv::range(session_id, b"k2", b"").unwrap(), + vec![ + (b"k2".to_vec(), b"v2".to_vec()), + (b"k3".to_vec(), b"v3".to_vec()), + ] + ); + + let create = SQLStmtText::new( + "CREATE TABLE t(id INT PRIMARY KEY, v TEXT); INSERT INTO t(id, v) VALUES (1, 'a');" + .to_string(), + ); + assert_eq!(sqlite::mudu_batch(session_id, &create, &()).unwrap(), 1); + + let conn = sqlite::open_connection().unwrap(); + let selected: String = conn + .query_row("SELECT v FROM t WHERE id = 1", [], |row| row.get(0)) + .unwrap(); + assert_eq!(selected, "a"); + + sqlite::mudu_close(session_id).unwrap(); + assert!(kv::ensure_session_exists(session_id).is_err()); +} + +#[test] +fn backend_batch_attempts_mudud_driver_request_instead_of_not_implemented() { + let _guard = test_lock().lock().unwrap(); + config::reset_db_path_override_for_test(); + with_connection_env("mudud://127.0.0.1:9527/default", || { + let stmt = SQLStmtText::new("SELECT 1".to_string()); + let err = backend::mudu_batch(1, &stmt, &()).unwrap_err(); + let message = err.to_string(); + assert!(!message.contains("batch syscall is not implemented for mudud adapter")); + }); +} + +#[test] +fn sqlite_async_session_kv_query_command_and_batch_work() { + let _guard = test_lock().lock().unwrap(); + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + config::reset_db_path_override_for_test(); + let db_path = temp_db_path("sqlite_async"); + config::set_db_path(&db_path); + + let session_id = backend::mudu_open_async(0).await.unwrap(); + backend::mudu_put_async(session_id, b"k2", b"v2").await.unwrap(); + backend::mudu_put_async(session_id, b"k1", b"v1").await.unwrap(); + + assert_eq!( + backend::mudu_get_async(session_id, b"k1").await.unwrap(), + Some(b"v1".to_vec()) + ); + assert_eq!( + backend::mudu_range_async(session_id, b"k1", b"").await.unwrap(), + vec![ + (b"k1".to_vec(), b"v1".to_vec()), + (b"k2".to_vec(), b"v2".to_vec()), + ] + ); + + let setup = SQLStmtText::new( + "CREATE TABLE demo(id INT PRIMARY KEY, v TEXT); INSERT INTO demo(id, v) VALUES (1, 'a');" + .to_string(), + ); + assert_eq!(backend::mudu_batch_async(session_id, &setup, &()).await.unwrap(), 1); + + let insert = SQLStmtText::new("INSERT INTO demo(id, v) VALUES (?1, ?2)".to_string()); + assert_eq!( + backend::mudu_command_async(session_id, &insert, &(2_i32, String::from("b"))) + .await + .unwrap(), + 1 + ); + + let query = SQLStmtText::new("SELECT v FROM demo WHERE id = ?1".to_string()); + let rows = backend::mudu_query_async::(session_id, &query, &(2_i32,)) + .await + .unwrap(); + assert_eq!(rows.next_record().unwrap(), Some("b".to_string())); + assert_eq!(rows.next_record().unwrap(), None); + + backend::mudu_close_async(session_id).await.unwrap(); + assert!(kv::ensure_session_exists(session_id).is_err()); + }); +} diff --git a/mudu_api/rust/src/universal/test_uni.rs b/mudu_api/rust/src/universal/test_uni.rs index 839c0a9..5ae779f 100644 --- a/mudu_api/rust/src/universal/test_uni.rs +++ b/mudu_api/rust/src/universal/test_uni.rs @@ -1,24 +1,287 @@ #[cfg(test)] mod tests { + use crate::universal::uni_command_argv::UniCommandArgv; use crate::universal::uni_dat_type::UniDatType; + use crate::universal::uni_dat_value::UniDatValue; + use crate::universal::uni_error::UniError; + use crate::universal::uni_get_result::UniGetResult; + use crate::universal::uni_key_value::UniKeyValue; + use crate::universal::uni_oid::UniOid; use crate::universal::uni_primitive::UniPrimitive; + use crate::universal::uni_primitive_value::UniPrimitiveValue; + use crate::universal::uni_procedure_param::UniProcedureParam; + use crate::universal::uni_procedure_result::UniProcedureResult; + use crate::universal::uni_query_argv::UniQueryArgv; + use crate::universal::uni_query_result::UniQueryResult; use crate::universal::uni_record_type::{UniRecordField, UniRecordType}; - use mudu::common::serde_utils::{deserialize_from_json, serialize_to_json, serialize_to_vec}; + use crate::universal::uni_range_result::UniRangeResult; + use crate::universal::uni_result::UniResult; + use crate::universal::uni_result_set::UniResultSet; + use crate::universal::uni_result_type::UniResultType; + use crate::universal::uni_sql_param::UniSqlParam; + use crate::universal::uni_sql_stmt::UniSqlStmt; + use crate::universal::uni_tuple_row::UniTupleRow; + use mudu::common::serde_utils::{ + deserialize_from, deserialize_from_json, serialize_to_json, serialize_to_vec, + }; + use serde::Serialize; + use serde::de::DeserializeOwned; + use std::fmt::Debug; + + fn assert_json_and_binary_roundtrip(value: &T) + where + T: Serialize + DeserializeOwned + Clone + Debug + 'static, + { + let json = serialize_to_json(value).unwrap(); + let binary = serialize_to_vec(value).unwrap(); + + let decoded_json: T = deserialize_from_json(json.as_str()).unwrap(); + let (decoded_binary, used): (T, u64) = deserialize_from(binary.as_slice()).unwrap(); + + let json_after = serialize_to_json(&decoded_json).unwrap(); + let binary_after = serialize_to_vec(&decoded_binary).unwrap(); + + assert_eq!(json_after, json); + assert_eq!(binary_after, binary); + assert_eq!(used as usize, binary.len()); + } + + fn sample_oid() -> UniOid { + UniOid { h: 7, l: 42 } + } + + fn sample_record_type() -> UniRecordType { + UniRecordType { + record_name: "vote_record".to_string(), + record_fields: vec![ + UniRecordField { + field_name: "id".to_string(), + field_type: UniDatType::Primitive(UniPrimitive::U128), + }, + UniRecordField { + field_name: "name".to_string(), + field_type: UniDatType::Primitive(UniPrimitive::String), + }, + UniRecordField { + field_name: "tags".to_string(), + field_type: UniDatType::Array(Box::new(UniDatType::Primitive( + UniPrimitive::String, + ))), + }, + ], + } + } + + fn sample_dat_type() -> UniDatType { + UniDatType::Record(UniRecordType { + record_name: "envelope".to_string(), + record_fields: vec![ + UniRecordField { + field_name: "meta".to_string(), + field_type: UniDatType::Tuple(vec![ + UniDatType::Primitive(UniPrimitive::U64), + UniDatType::Option(Box::new(UniDatType::Primitive(UniPrimitive::String))), + ]), + }, + UniRecordField { + field_name: "payload".to_string(), + field_type: UniDatType::Result(UniResultType { + ok: Some(Box::new(UniDatType::Array(Box::new(UniDatType::Primitive( + UniPrimitive::I32, + ))))), + err: Some(Box::new(UniDatType::Identifier("ErrCode".to_string()))), + }), + }, + UniRecordField { + field_name: "blob".to_string(), + field_type: UniDatType::Binary, + }, + ], + }) + } + + fn sample_dat_value() -> UniDatValue { + UniDatValue::Record(vec![ + UniDatValue::Array(vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_i32(10)), + UniDatValue::Primitive(UniPrimitiveValue::from_i32(-4)), + ]), + UniDatValue::Record(vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_bool(true)), + UniDatValue::Primitive(UniPrimitiveValue::from_string("ok".to_string())), + ]), + UniDatValue::Binary(vec![1, 2, 3, 4, 200]), + ]) + } + + fn sample_query_result() -> UniQueryResult { + UniQueryResult { + tuple_desc: sample_record_type(), + result_set: UniResultSet { + eof: false, + row_set: vec![UniTupleRow { + fields: vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_u128(99)), + UniDatValue::Primitive(UniPrimitiveValue::from_string( + "alice".to_string(), + )), + UniDatValue::Array(vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_string( + "x".to_string(), + )), + UniDatValue::Primitive(UniPrimitiveValue::from_string( + "y".to_string(), + )), + ]), + ], + }], + cursor: vec![9, 8, 7], + }, + } + } #[test] fn test_uni_dat_type() { - let uni_dat_ty = UniDatType::Record(UniRecordType { - record_name: "record".to_string(), - record_fields: vec![UniRecordField { - field_name: "field1".to_string(), - field_type: UniDatType::Primitive(UniPrimitive::I16), - }], - }); + let uni_dat_ty = sample_dat_type(); + assert_json_and_binary_roundtrip(&uni_dat_ty); + let json = serialize_to_json(&uni_dat_ty).unwrap(); - let binary = serialize_to_vec(&uni_dat_ty).unwrap(); - println!("{}", json); - println!("{}", hex::encode(&binary)); let uni_dat_ty2: UniDatType = deserialize_from_json(json.as_str()).unwrap(); - println!("{:?}", uni_dat_ty2); + let record = uni_dat_ty2.as_record().expect("record dat type"); + assert_eq!(record.record_name, "envelope"); + assert_eq!(record.record_fields.len(), 3); + assert!(record.record_fields[2] + .field_type + .as_identifier() + .is_none()); + } + + #[test] + fn test_uni_primitive_value_roundtrip_matrix() { + let cases = vec![ + UniPrimitiveValue::from_bool(true), + UniPrimitiveValue::from_u8(3), + UniPrimitiveValue::from_i8(7), + UniPrimitiveValue::from_u16(16), + UniPrimitiveValue::from_i16(-16), + UniPrimitiveValue::from_u32(32), + UniPrimitiveValue::from_i32(-32), + UniPrimitiveValue::from_u64(64), + UniPrimitiveValue::from_u128(128), + UniPrimitiveValue::from_i64(-64), + UniPrimitiveValue::from_i128(-128), + UniPrimitiveValue::from_f32(3.25), + UniPrimitiveValue::from_f64(-9.5), + UniPrimitiveValue::from_char('z'), + UniPrimitiveValue::from_string("hello".to_string()), + ]; + + for value in cases { + assert_json_and_binary_roundtrip(&value); + } + } + + #[test] + fn test_uni_dat_value_roundtrip_matrix() { + let cases = vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_string("row".to_string())), + UniDatValue::Array(vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_u64(1)), + UniDatValue::Primitive(UniPrimitiveValue::from_u64(2)), + ]), + sample_dat_value(), + UniDatValue::Binary(vec![0, 1, 2, 3, 255]), + ]; + + for value in cases { + assert_json_and_binary_roundtrip(&value); + } + } + + #[test] + fn test_uni_result_roundtrip_for_ok_and_err() { + let ok: UniResult = UniResult::Ok(sample_dat_type()); + let err: UniResult = UniResult::Err(UniError { + err_code: 404, + err_msg: "not found".to_string(), + err_src: "unit-test".to_string(), + err_loc: "test_uni".to_string(), + }); + + assert_json_and_binary_roundtrip(&ok); + assert_json_and_binary_roundtrip(&err); + } + + #[test] + fn test_universal_request_and_result_struct_roundtrip() { + let sql_stmt = UniSqlStmt { + sql_string: "select id, name from users where id = ?".to_string(), + }; + let sql_param = UniSqlParam { + params: vec![UniDatValue::Primitive(UniPrimitiveValue::from_u128(7))], + }; + + let query_argv = UniQueryArgv { + oid: sample_oid(), + query: sql_stmt.clone(), + param_list: sql_param.clone(), + }; + let command_argv = UniCommandArgv { + oid: sample_oid(), + command: sql_stmt, + param_list: sql_param, + }; + let procedure_param = UniProcedureParam { + procedure: 88, + session: sample_oid(), + param_list: vec![sample_dat_value()], + }; + let procedure_result = UniProcedureResult { + return_list: vec![sample_dat_value()], + }; + let get_result = UniGetResult { + value: Some(UniDatValue::Primitive(UniPrimitiveValue::from_string( + "payload".to_string(), + ))), + }; + let range_result = UniRangeResult { + items: vec![UniKeyValue { + key: UniDatValue::Primitive(UniPrimitiveValue::from_u64(1)), + value: sample_dat_value(), + }], + }; + let query_result = sample_query_result(); + + assert_json_and_binary_roundtrip(&query_argv); + assert_json_and_binary_roundtrip(&command_argv); + assert_json_and_binary_roundtrip(&procedure_param); + assert_json_and_binary_roundtrip(&procedure_result); + assert_json_and_binary_roundtrip(&get_result); + assert_json_and_binary_roundtrip(&range_result); + assert_json_and_binary_roundtrip(&query_result); + } + + #[test] + fn test_uni_dat_type_and_value_reject_invalid_tags() { + let invalid_dat_type_json = "[99,0]"; + let invalid_dat_value_json = "[99,0]"; + let invalid_primitive_json = "[99,0]"; + + assert!(deserialize_from_json::(invalid_dat_type_json).is_err()); + assert!(deserialize_from_json::(invalid_dat_value_json).is_err()); + assert!(deserialize_from_json::(invalid_primitive_json).is_err()); + } + + #[test] + fn test_uni_result_rejects_invalid_payload_shape() { + assert!(deserialize_from_json::>("{}").is_err()); + assert!(deserialize_from_json::>("{\"99\":{}}").is_err()); + } + + #[test] + fn test_uni_dat_type_binary_rejects_truncated_payload() { + let binary = serialize_to_vec(&sample_dat_type()).unwrap(); + let truncated = &binary[..binary.len() - 1]; + assert!(deserialize_from::(truncated).is_err()); } } diff --git a/mudu_api/rust/src/universal/uni_result.rs b/mudu_api/rust/src/universal/uni_result.rs index 59aa065..91622da 100644 --- a/mudu_api/rust/src/universal/uni_result.rs +++ b/mudu_api/rust/src/universal/uni_result.rs @@ -87,7 +87,7 @@ where where D: Deserializer<'de>, { - deserializer.deserialize_seq(UniResultVisitor::default()) + deserializer.deserialize_map(UniResultVisitor::default()) } } diff --git a/mudu_binding/src/universal/test_uni.rs b/mudu_binding/src/universal/test_uni.rs index 839c0a9..5ae779f 100644 --- a/mudu_binding/src/universal/test_uni.rs +++ b/mudu_binding/src/universal/test_uni.rs @@ -1,24 +1,287 @@ #[cfg(test)] mod tests { + use crate::universal::uni_command_argv::UniCommandArgv; use crate::universal::uni_dat_type::UniDatType; + use crate::universal::uni_dat_value::UniDatValue; + use crate::universal::uni_error::UniError; + use crate::universal::uni_get_result::UniGetResult; + use crate::universal::uni_key_value::UniKeyValue; + use crate::universal::uni_oid::UniOid; use crate::universal::uni_primitive::UniPrimitive; + use crate::universal::uni_primitive_value::UniPrimitiveValue; + use crate::universal::uni_procedure_param::UniProcedureParam; + use crate::universal::uni_procedure_result::UniProcedureResult; + use crate::universal::uni_query_argv::UniQueryArgv; + use crate::universal::uni_query_result::UniQueryResult; use crate::universal::uni_record_type::{UniRecordField, UniRecordType}; - use mudu::common::serde_utils::{deserialize_from_json, serialize_to_json, serialize_to_vec}; + use crate::universal::uni_range_result::UniRangeResult; + use crate::universal::uni_result::UniResult; + use crate::universal::uni_result_set::UniResultSet; + use crate::universal::uni_result_type::UniResultType; + use crate::universal::uni_sql_param::UniSqlParam; + use crate::universal::uni_sql_stmt::UniSqlStmt; + use crate::universal::uni_tuple_row::UniTupleRow; + use mudu::common::serde_utils::{ + deserialize_from, deserialize_from_json, serialize_to_json, serialize_to_vec, + }; + use serde::Serialize; + use serde::de::DeserializeOwned; + use std::fmt::Debug; + + fn assert_json_and_binary_roundtrip(value: &T) + where + T: Serialize + DeserializeOwned + Clone + Debug + 'static, + { + let json = serialize_to_json(value).unwrap(); + let binary = serialize_to_vec(value).unwrap(); + + let decoded_json: T = deserialize_from_json(json.as_str()).unwrap(); + let (decoded_binary, used): (T, u64) = deserialize_from(binary.as_slice()).unwrap(); + + let json_after = serialize_to_json(&decoded_json).unwrap(); + let binary_after = serialize_to_vec(&decoded_binary).unwrap(); + + assert_eq!(json_after, json); + assert_eq!(binary_after, binary); + assert_eq!(used as usize, binary.len()); + } + + fn sample_oid() -> UniOid { + UniOid { h: 7, l: 42 } + } + + fn sample_record_type() -> UniRecordType { + UniRecordType { + record_name: "vote_record".to_string(), + record_fields: vec![ + UniRecordField { + field_name: "id".to_string(), + field_type: UniDatType::Primitive(UniPrimitive::U128), + }, + UniRecordField { + field_name: "name".to_string(), + field_type: UniDatType::Primitive(UniPrimitive::String), + }, + UniRecordField { + field_name: "tags".to_string(), + field_type: UniDatType::Array(Box::new(UniDatType::Primitive( + UniPrimitive::String, + ))), + }, + ], + } + } + + fn sample_dat_type() -> UniDatType { + UniDatType::Record(UniRecordType { + record_name: "envelope".to_string(), + record_fields: vec![ + UniRecordField { + field_name: "meta".to_string(), + field_type: UniDatType::Tuple(vec![ + UniDatType::Primitive(UniPrimitive::U64), + UniDatType::Option(Box::new(UniDatType::Primitive(UniPrimitive::String))), + ]), + }, + UniRecordField { + field_name: "payload".to_string(), + field_type: UniDatType::Result(UniResultType { + ok: Some(Box::new(UniDatType::Array(Box::new(UniDatType::Primitive( + UniPrimitive::I32, + ))))), + err: Some(Box::new(UniDatType::Identifier("ErrCode".to_string()))), + }), + }, + UniRecordField { + field_name: "blob".to_string(), + field_type: UniDatType::Binary, + }, + ], + }) + } + + fn sample_dat_value() -> UniDatValue { + UniDatValue::Record(vec![ + UniDatValue::Array(vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_i32(10)), + UniDatValue::Primitive(UniPrimitiveValue::from_i32(-4)), + ]), + UniDatValue::Record(vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_bool(true)), + UniDatValue::Primitive(UniPrimitiveValue::from_string("ok".to_string())), + ]), + UniDatValue::Binary(vec![1, 2, 3, 4, 200]), + ]) + } + + fn sample_query_result() -> UniQueryResult { + UniQueryResult { + tuple_desc: sample_record_type(), + result_set: UniResultSet { + eof: false, + row_set: vec![UniTupleRow { + fields: vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_u128(99)), + UniDatValue::Primitive(UniPrimitiveValue::from_string( + "alice".to_string(), + )), + UniDatValue::Array(vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_string( + "x".to_string(), + )), + UniDatValue::Primitive(UniPrimitiveValue::from_string( + "y".to_string(), + )), + ]), + ], + }], + cursor: vec![9, 8, 7], + }, + } + } #[test] fn test_uni_dat_type() { - let uni_dat_ty = UniDatType::Record(UniRecordType { - record_name: "record".to_string(), - record_fields: vec![UniRecordField { - field_name: "field1".to_string(), - field_type: UniDatType::Primitive(UniPrimitive::I16), - }], - }); + let uni_dat_ty = sample_dat_type(); + assert_json_and_binary_roundtrip(&uni_dat_ty); + let json = serialize_to_json(&uni_dat_ty).unwrap(); - let binary = serialize_to_vec(&uni_dat_ty).unwrap(); - println!("{}", json); - println!("{}", hex::encode(&binary)); let uni_dat_ty2: UniDatType = deserialize_from_json(json.as_str()).unwrap(); - println!("{:?}", uni_dat_ty2); + let record = uni_dat_ty2.as_record().expect("record dat type"); + assert_eq!(record.record_name, "envelope"); + assert_eq!(record.record_fields.len(), 3); + assert!(record.record_fields[2] + .field_type + .as_identifier() + .is_none()); + } + + #[test] + fn test_uni_primitive_value_roundtrip_matrix() { + let cases = vec![ + UniPrimitiveValue::from_bool(true), + UniPrimitiveValue::from_u8(3), + UniPrimitiveValue::from_i8(7), + UniPrimitiveValue::from_u16(16), + UniPrimitiveValue::from_i16(-16), + UniPrimitiveValue::from_u32(32), + UniPrimitiveValue::from_i32(-32), + UniPrimitiveValue::from_u64(64), + UniPrimitiveValue::from_u128(128), + UniPrimitiveValue::from_i64(-64), + UniPrimitiveValue::from_i128(-128), + UniPrimitiveValue::from_f32(3.25), + UniPrimitiveValue::from_f64(-9.5), + UniPrimitiveValue::from_char('z'), + UniPrimitiveValue::from_string("hello".to_string()), + ]; + + for value in cases { + assert_json_and_binary_roundtrip(&value); + } + } + + #[test] + fn test_uni_dat_value_roundtrip_matrix() { + let cases = vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_string("row".to_string())), + UniDatValue::Array(vec![ + UniDatValue::Primitive(UniPrimitiveValue::from_u64(1)), + UniDatValue::Primitive(UniPrimitiveValue::from_u64(2)), + ]), + sample_dat_value(), + UniDatValue::Binary(vec![0, 1, 2, 3, 255]), + ]; + + for value in cases { + assert_json_and_binary_roundtrip(&value); + } + } + + #[test] + fn test_uni_result_roundtrip_for_ok_and_err() { + let ok: UniResult = UniResult::Ok(sample_dat_type()); + let err: UniResult = UniResult::Err(UniError { + err_code: 404, + err_msg: "not found".to_string(), + err_src: "unit-test".to_string(), + err_loc: "test_uni".to_string(), + }); + + assert_json_and_binary_roundtrip(&ok); + assert_json_and_binary_roundtrip(&err); + } + + #[test] + fn test_universal_request_and_result_struct_roundtrip() { + let sql_stmt = UniSqlStmt { + sql_string: "select id, name from users where id = ?".to_string(), + }; + let sql_param = UniSqlParam { + params: vec![UniDatValue::Primitive(UniPrimitiveValue::from_u128(7))], + }; + + let query_argv = UniQueryArgv { + oid: sample_oid(), + query: sql_stmt.clone(), + param_list: sql_param.clone(), + }; + let command_argv = UniCommandArgv { + oid: sample_oid(), + command: sql_stmt, + param_list: sql_param, + }; + let procedure_param = UniProcedureParam { + procedure: 88, + session: sample_oid(), + param_list: vec![sample_dat_value()], + }; + let procedure_result = UniProcedureResult { + return_list: vec![sample_dat_value()], + }; + let get_result = UniGetResult { + value: Some(UniDatValue::Primitive(UniPrimitiveValue::from_string( + "payload".to_string(), + ))), + }; + let range_result = UniRangeResult { + items: vec![UniKeyValue { + key: UniDatValue::Primitive(UniPrimitiveValue::from_u64(1)), + value: sample_dat_value(), + }], + }; + let query_result = sample_query_result(); + + assert_json_and_binary_roundtrip(&query_argv); + assert_json_and_binary_roundtrip(&command_argv); + assert_json_and_binary_roundtrip(&procedure_param); + assert_json_and_binary_roundtrip(&procedure_result); + assert_json_and_binary_roundtrip(&get_result); + assert_json_and_binary_roundtrip(&range_result); + assert_json_and_binary_roundtrip(&query_result); + } + + #[test] + fn test_uni_dat_type_and_value_reject_invalid_tags() { + let invalid_dat_type_json = "[99,0]"; + let invalid_dat_value_json = "[99,0]"; + let invalid_primitive_json = "[99,0]"; + + assert!(deserialize_from_json::(invalid_dat_type_json).is_err()); + assert!(deserialize_from_json::(invalid_dat_value_json).is_err()); + assert!(deserialize_from_json::(invalid_primitive_json).is_err()); + } + + #[test] + fn test_uni_result_rejects_invalid_payload_shape() { + assert!(deserialize_from_json::>("{}").is_err()); + assert!(deserialize_from_json::>("{\"99\":{}}").is_err()); + } + + #[test] + fn test_uni_dat_type_binary_rejects_truncated_payload() { + let binary = serialize_to_vec(&sample_dat_type()).unwrap(); + let truncated = &binary[..binary.len() - 1]; + assert!(deserialize_from::(truncated).is_err()); } } diff --git a/mudu_binding/src/universal/uni_result.rs b/mudu_binding/src/universal/uni_result.rs index 59aa065..91622da 100644 --- a/mudu_binding/src/universal/uni_result.rs +++ b/mudu_binding/src/universal/uni_result.rs @@ -87,7 +87,7 @@ where where D: Deserializer<'de>, { - deserializer.deserialize_seq(UniResultVisitor::default()) + deserializer.deserialize_map(UniResultVisitor::default()) } } diff --git a/mudu_cli/Cargo.toml b/mudu_cli/Cargo.toml index e6a4ab6..d675bff 100644 --- a/mudu_cli/Cargo.toml +++ b/mudu_cli/Cargo.toml @@ -11,6 +11,7 @@ path = "src/main.rs" mudu = { workspace = true } mudu_binding = { workspace = true } mudu_contract = { workspace = true } +mudu_type = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } diff --git a/mudu_cli/README.md b/mudu_cli/README.md index 730c0a8..fabb25f 100644 --- a/mudu_cli/README.md +++ b/mudu_cli/README.md @@ -19,13 +19,13 @@ It talks directly to the server TCP protocol and exposes these operations: Query: ```bash -mcli --addr 127.0.0.1:9000 command --json '{"app_name":"demo","sql":"select 1"}' +mcli --addr 127.0.0.1:9527 command --json '{"app_name":"demo","sql":"select 1"}' ``` Put: ```bash -mcli put --json '{ +mcli --addr 127.0.0.1:9527 put --json '{ "key": "user-1", "value": "value-1" }' @@ -34,7 +34,7 @@ mcli put --json '{ Get: ```bash -mcli get --json '{ +mcli --addr 127.0.0.1:9527 get --json '{ "key": "user-1" }' ``` @@ -42,7 +42,7 @@ mcli get --json '{ Range scan: ```bash -mcli range --json '{ +mcli --addr 127.0.0.1:9527 range --json '{ "start_key": "a", "end_key": "z" }' @@ -51,7 +51,7 @@ mcli range --json '{ Invoke: ```bash -mcli invoke --json '{ +mcli --addr 127.0.0.1:9527 invoke --json '{ "procedure_name": "app/mod/proc", "procedure_parameters": {"base64": "cGF5bG9hZA=="} }' @@ -60,13 +60,13 @@ mcli invoke --json '{ Install `.mpk` through the management HTTP API: ```bash -mcli app-install --mpk target/wasm32-wasip2/release/key-value.mpk +mcli --http-addr 127.0.0.1:8300 app-install --mpk target/wasm32-wasip2/release/key-value.mpk ``` Invoke an installed procedure through the TCP protocol: ```bash -mcli app-invoke --app kv --module key_value --proc kv_read --json '{ +mcli --addr 127.0.0.1:9527 --http-addr 127.0.0.1:8300 app-invoke --app kv --module key_value --proc kv_read --json '{ "user_key": "user-1" }' ``` @@ -79,4 +79,4 @@ JSON request bodies can be supplied in three ways: - `--json-file request.json` - `--json-file -` to read from stdin -`put`, `get`, and `range` accept ordinary JSON values for keys and values. `mcli` still creates and injects a temporary session automatically. +`put`, `get`, and `range` accept ordinary JSON values for keys and values. `mcli` still creates and injects a temporary session automatically for those commands. diff --git a/mudu_cli/src/binding_api.rs b/mudu_cli/src/binding_api.rs index 2fd3b98..2a4dfa0 100644 --- a/mudu_cli/src/binding_api.rs +++ b/mudu_cli/src/binding_api.rs @@ -1,6 +1,9 @@ use crate::client::async_client::{AsyncClient, AsyncClientImpl}; -use crate::management::fetch_server_topology; +use crate::management::{ + fetch_server_topology, is_server_topology_unsupported, route_partition, +}; use base64::Engine; +use mudu::common::serde_utils; use mudu_contract::protocol::{ ClientRequest, GetRequest, ProcedureInvokeRequest, PutRequest, RangeScanRequest, SessionCloseRequest, SessionCreateRequest, @@ -18,14 +21,14 @@ pub enum MuduCliBindingError { } #[derive(Debug, Clone, uniffi::Record)] -pub struct MuduRow { - pub values: Vec, +pub struct MuduTupleRowBinding { + pub tuple_value_bytes: Vec, } #[derive(Debug, Clone, uniffi::Record)] pub struct MuduServerResponseBinding { - pub columns: Vec, - pub rows: Vec, + pub row_desc_bytes: Vec, + pub rows: Vec, pub affected_rows: u64, pub error: Option, } @@ -49,6 +52,17 @@ pub struct ServerTopologyBinding { pub workers: Vec, } +#[derive(Debug, Clone, uniffi::Record)] +pub struct PartitionRouteEntryBinding { + pub partition_id: String, + pub worker_id: String, +} + +#[derive(Debug, Clone, uniffi::Record)] +pub struct PartitionRouteResponseBinding { + pub routes: Vec, +} + #[derive(uniffi::Object)] pub struct MuduTcpClient { runtime: Mutex, @@ -199,22 +213,19 @@ pub fn fetch_server_topology_binding( let topology = runtime .block_on(fetch_server_topology(&http_addr)) .map_err(MuduCliBindingError::Message)?; - Ok(ServerTopologyBinding { - worker_count: topology.worker_count as u64, - workers: topology - .workers - .into_iter() - .map(|worker| WorkerTopologyBinding { - worker_index: worker.worker_index as u64, - worker_id: worker.worker_id.to_string(), - partitions: worker - .partitions - .into_iter() - .map(|partition| partition.to_string()) - .collect(), - }) - .collect(), - }) + Ok(to_server_topology_binding(topology)) +} + +#[uniffi::export] +pub fn try_fetch_server_topology_binding( + http_addr: String, +) -> Result, MuduCliBindingError> { + let runtime = new_runtime()?; + match runtime.block_on(fetch_server_topology(&http_addr)) { + Ok(topology) => Ok(Some(to_server_topology_binding(topology))), + Err(err) if is_server_topology_unsupported(&err) => Ok(None), + Err(err) => Err(MuduCliBindingError::Message(err)), + } } #[uniffi::export] @@ -230,20 +241,74 @@ pub fn install_app_package( Ok(true) } +#[uniffi::export] +pub fn route_partition_binding( + http_addr: String, + rule_name: String, + key: Option>, + start: Option>, + end: Option>, +) -> Result { + let runtime = new_runtime()?; + let response = runtime + .block_on(route_partition(&http_addr, &rule_name, key, start, end)) + .map_err(MuduCliBindingError::Message)?; + Ok(to_partition_route_response_binding(response)) +} + fn convert_server_response(response: mudu_contract::protocol::ServerResponse) -> MuduServerResponseBinding { + let row_desc_bytes = serde_utils::serialize_sized_to_vec(response.row_desc()) + .unwrap_or_default(); MuduServerResponseBinding { - columns: response.columns().to_vec(), + row_desc_bytes, rows: response .rows() .iter() - .cloned() - .map(|values| MuduRow { values }) + .map(|row| MuduTupleRowBinding { + tuple_value_bytes: serde_utils::serialize_sized_to_vec(row).unwrap_or_default(), + }) .collect(), affected_rows: response.affected_rows(), error: response.error().map(|value| value.to_string()), } } +fn to_server_topology_binding( + topology: crate::management::ServerTopology, +) -> ServerTopologyBinding { + ServerTopologyBinding { + worker_count: topology.worker_count as u64, + workers: topology + .workers + .into_iter() + .map(|worker| WorkerTopologyBinding { + worker_index: worker.worker_index as u64, + worker_id: worker.worker_id.to_string(), + partitions: worker + .partitions + .into_iter() + .map(|partition| partition.to_string()) + .collect(), + }) + .collect(), + } +} + +fn to_partition_route_response_binding( + response: crate::management::PartitionRouteResponse, +) -> PartitionRouteResponseBinding { + PartitionRouteResponseBinding { + routes: response + .routes + .into_iter() + .map(|route| PartitionRouteEntryBinding { + partition_id: route.partition_id.to_string(), + worker_id: route.worker_id.to_string(), + }) + .collect(), + } +} + fn parse_session_id(session_id: &str) -> Result { session_id.parse::().map_err(|e| { MuduCliBindingError::Message(format!( diff --git a/mudu_cli/src/client/async_client.rs b/mudu_cli/src/client/async_client.rs index 0331c27..1b3958d 100644 --- a/mudu_cli/src/client/async_client.rs +++ b/mudu_cli/src/client/async_client.rs @@ -9,9 +9,9 @@ use mudu_contract::protocol::{ SessionCreateResponse, decode_error_response, decode_get_response, decode_procedure_invoke_response, decode_put_response, decode_range_scan_response, decode_server_response, decode_session_close_response, decode_session_create_response, - encode_batch_request, encode_client_request_with_message_type, encode_get_request, encode_procedure_invoke_request, - encode_put_request, encode_range_scan_request, encode_session_close_request, - encode_session_create_request, + encode_batch_request, encode_client_request_with_message_type, encode_get_request, + encode_procedure_invoke_request, encode_put_request, encode_range_scan_request, + encode_session_close_request, encode_session_create_request, }; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; @@ -181,6 +181,12 @@ mod tests { encode_put_response, encode_range_scan_response, encode_server_response, encode_session_close_response, encode_session_create_response, }; + use mudu_contract::tuple::datum_desc::DatumDesc; + use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; + use mudu_contract::tuple::tuple_value::TupleValue; + use mudu_type::dat_type::DatType; + use mudu_type::dat_type_id::DatTypeID; + use mudu_type::dat_value::DatValue; use std::io::{Read, Write}; use std::net::TcpListener; use std::thread; @@ -214,8 +220,11 @@ mod tests { &encode_server_response( query_frame.header().request_id(), &ServerResponse::new( - vec!["value".to_string()], - vec![vec!["1".to_string()]], + TupleFieldDesc::new(vec![DatumDesc::new( + "value".to_string(), + DatType::default_for(DatTypeID::String), + )]), + vec![TupleValue::from(vec![DatValue::from_string("1".to_string())])], 0, None, ), @@ -232,7 +241,7 @@ mod tests { .write_all( &encode_server_response( execute_frame.header().request_id(), - &ServerResponse::new(vec![], vec![], 2, None), + &ServerResponse::new(TupleFieldDesc::new(vec![]), vec![], 2, None), ) .unwrap(), ) @@ -244,8 +253,8 @@ mod tests { .query(ClientRequest::new("demo", "select 1")) .await .unwrap(); - assert_eq!(query.columns(), &["value".to_string()]); - assert_eq!(query.rows(), &[vec!["1".to_string()]]); + assert_eq!(query.row_desc().fields()[0].name(), "value"); + assert_eq!(query.rows()[0].values()[0].expect_string(), "1"); let execute = client .execute(ClientRequest::new("demo", "delete from t")) diff --git a/mudu_cli/src/client/client.rs b/mudu_cli/src/client/client.rs index 682d1b9..ea01302 100644 --- a/mudu_cli/src/client/client.rs +++ b/mudu_cli/src/client/client.rs @@ -6,10 +6,10 @@ use mudu_contract::protocol::{ PutRequest, RangeScanRequest, ServerResponse, SessionCloseRequest, SessionCreateRequest, decode_error_response, decode_get_response, decode_procedure_invoke_response, decode_put_response, decode_range_scan_response, decode_server_response, - decode_session_close_response, decode_session_create_response, encode_batch_request, encode_client_request, - encode_client_request_with_message_type, encode_get_request, encode_procedure_invoke_request, - encode_put_request, encode_range_scan_request, encode_session_close_request, - encode_session_create_request, + decode_session_close_response, decode_session_create_response, encode_batch_request, + encode_client_request, encode_client_request_with_message_type, encode_get_request, + encode_procedure_invoke_request, encode_put_request, encode_range_scan_request, + encode_session_close_request, encode_session_create_request, }; use std::io::{Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; diff --git a/mudu_cli/src/client/json_client.rs b/mudu_cli/src/client/json_client.rs index bcf6c67..59f68ab 100644 --- a/mudu_cli/src/client/json_client.rs +++ b/mudu_cli/src/client/json_client.rs @@ -8,7 +8,10 @@ use mudu_binding::universal::uni_oid::UniOid; use mudu_binding::universal::uni_primitive_value::UniPrimitiveValue; use mudu_contract::protocol::{ ClientRequest, GetRequest, KeyValue, ProcedureInvokeRequest, PutRequest, RangeScanRequest, + ServerResponse, }; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::datum::DatumDyn; use serde::Deserialize; use serde::de::{self, Deserializer}; use serde_json::{Value, json}; @@ -46,8 +49,7 @@ where } else { self.inner.query(client_request).await? }; - serde_json::to_value(response) - .map_err(|e| m_error!(EC::EncodeErr, "encode json command response error", e)) + server_response_to_json(&response) } pub async fn put(&mut self, request: Value) -> RS { @@ -302,6 +304,44 @@ fn key_value_to_json(key_value: KeyValue) -> RS { })) } +fn server_response_to_json(response: &ServerResponse) -> RS { + let columns = response + .row_desc() + .fields() + .iter() + .map(|field| Value::String(field.name().to_string())) + .collect::>(); + + let rows = response + .rows() + .iter() + .map(|row| { + let values = row + .values() + .iter() + .zip(response.row_desc().fields().iter()) + .map(|(value, field_desc)| { + if field_desc.dat_type().dat_type_id() == DatTypeID::String { + Ok(Value::String(value.expect_string().clone())) + } else { + value + .to_textual(field_desc.dat_type()) + .map(|text| Value::String(text.into())) + } + }) + .collect::>>()?; + Ok(Value::Array(values)) + }) + .collect::>>()?; + + Ok(json!({ + "columns": columns, + "rows": rows, + "affected_rows": response.affected_rows(), + "error": response.error(), + })) +} + #[cfg(test)] mod tests { use super::*; @@ -312,6 +352,12 @@ mod tests { ServerResponse, SessionCloseRequest, SessionCloseResponse, SessionCreateRequest, SessionCreateResponse, }; + use mudu_contract::tuple::datum_desc::DatumDesc; + use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; + use mudu_contract::tuple::tuple_value::TupleValue; + use mudu_type::dat_type::DatType; + use mudu_type::dat_type_id::DatTypeID; + use mudu_type::dat_value::DatValue; struct MockAsyncIoUringTcpClient { last_query: Option, @@ -342,8 +388,11 @@ mod tests { async fn query(&mut self, request: ClientRequest) -> RS { self.last_query = Some(request); Ok(ServerResponse::new( - vec!["value".to_string()], - vec![vec!["1".to_string()]], + TupleFieldDesc::new(vec![DatumDesc::new( + "value".to_string(), + DatType::default_for(DatTypeID::String), + )]), + vec![TupleValue::from(vec![DatValue::from_string("1".to_string())])], 0, None, )) @@ -351,12 +400,12 @@ mod tests { async fn execute(&mut self, request: ClientRequest) -> RS { self.last_execute = Some(request); - Ok(ServerResponse::new(vec![], vec![], 2, None)) + Ok(ServerResponse::new(TupleFieldDesc::new(vec![]), vec![], 2, None)) } async fn batch(&mut self, request: ClientRequest) -> RS { self.last_batch = Some(request); - Ok(ServerResponse::new(vec![], vec![], 3, None)) + Ok(ServerResponse::new(TupleFieldDesc::new(vec![]), vec![], 3, None)) } async fn get(&mut self, request: GetRequest) -> RS { diff --git a/mudu_cli/src/main.rs b/mudu_cli/src/main.rs index dbb5d7d..2854567 100644 --- a/mudu_cli/src/main.rs +++ b/mudu_cli/src/main.rs @@ -16,11 +16,11 @@ type AppResult = Result; const CLI_EXAMPLES: &str = "\ Examples: - mcli --addr 127.0.0.1:9000 command --json '{\"app_name\":\"demo\",\"sql\":\"select 1\"}' - mcli put --json-file put.json - cat invoke.json | mcli invoke --json-file - - mcli app-install --mpk target/wasm32-wasip2/release/key-value.mpk - mcli app-invoke --app kv --module key_value --proc kv_read --json '{\"user_key\":\"user-1\"}'"; + mcli --addr 127.0.0.1:9527 command --json '{\"app_name\":\"demo\",\"sql\":\"select 1\"}' + mcli --addr 127.0.0.1:9527 put --json-file put.json + cat invoke.json | mcli --addr 127.0.0.1:9527 invoke --json-file - + mcli --http-addr 127.0.0.1:8300 app-install --mpk target/wasm32-wasip2/release/key-value.mpk + mcli --addr 127.0.0.1:9527 --http-addr 127.0.0.1:8300 app-invoke --app kv --module key_value --proc kv_read --json '{\"user_key\":\"user-1\"}'"; #[derive(Parser, Debug)] #[command(name = "mcli")] @@ -28,7 +28,7 @@ Examples: #[command(about = "TCP protocol client for MuduDB")] #[command(after_help = CLI_EXAMPLES)] struct Cli { - #[arg(long, global = true, default_value = "127.0.0.1:9000")] + #[arg(long, global = true, default_value = "127.0.0.1:9527")] addr: String, #[arg(long, global = true, default_value = "127.0.0.1:8300")] http_addr: String, @@ -121,7 +121,7 @@ async fn run(cli: Cli) -> AppResult<()> { .await .map_err(|e| format!("session-create for put failed: {}", e))? .session_id(); - let request = with_session_id(request, session_id)?; + let request = with_oid(request, session_id)?; let mut client = JsonClient::new(client); let response = client .put(request) @@ -143,7 +143,7 @@ async fn run(cli: Cli) -> AppResult<()> { .await .map_err(|e| format!("session-create for get failed: {}", e))? .session_id(); - let request = with_session_id(request, session_id)?; + let request = with_oid(request, session_id)?; let mut client = JsonClient::new(client); let response = client .get(request) @@ -165,7 +165,7 @@ async fn run(cli: Cli) -> AppResult<()> { .await .map_err(|e| format!("session-create for range failed: {}", e))? .session_id(); - let request = with_session_id(request, session_id)?; + let request = with_oid(request, session_id)?; let mut client = JsonClient::new(client); let response = client .range(request) @@ -187,7 +187,7 @@ async fn run(cli: Cli) -> AppResult<()> { .await .map_err(|e| format!("session-create for invoke failed: {}", e))? .session_id(); - let request = with_session_id(request, session_id)?; + let request = with_invoke_session_id(request, session_id)?; let mut client = JsonClient::new(client); let response = client .invoke(request) @@ -268,7 +268,7 @@ fn load_required_text(inline: Option, file: Option) -> AppResul } } -fn with_session_id(request: Value, session_id: u128) -> AppResult { +fn with_oid(request: Value, session_id: u128) -> AppResult { let mut request = request .as_object() .cloned() @@ -283,6 +283,15 @@ fn with_session_id(request: Value, session_id: u128) -> AppResult { Ok(Value::Object(request)) } +fn with_invoke_session_id(request: Value, session_id: u128) -> AppResult { + let mut request = request + .as_object() + .cloned() + .ok_or_else(|| "request JSON must be an object".to_string())?; + request.insert("session_id".to_string(), json!(session_id.to_string())); + Ok(Value::Object(request)) +} + fn read_special_text_input(text: String) -> AppResult { if text == "-" { read_stdin_to_string() @@ -511,12 +520,19 @@ mod tests { } #[test] - fn with_session_id_injects_string_value() { - let request = with_session_id(json!({"key": "user-1"}), 99).unwrap(); + fn with_oid_injects_oid_value() { + let request = with_oid(json!({"key": "user-1"}), 99).unwrap(); assert_eq!(request["oid"], json!({"h": 0, "l": 99})); assert_eq!(request["key"], json!("user-1")); } + #[test] + fn with_invoke_session_id_injects_session_id_string() { + let request = with_invoke_session_id(json!({"procedure_name": "app/mod/proc"}), 99).unwrap(); + assert_eq!(request["session_id"], json!("99")); + assert_eq!(request["procedure_name"], json!("app/mod/proc")); + } + fn unique_temp_path(prefix: &str) -> PathBuf { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/mudu_cli/src/management.rs b/mudu_cli/src/management.rs index 679d0d9..698a5ff 100644 --- a/mudu_cli/src/management.rs +++ b/mudu_cli/src/management.rs @@ -1,7 +1,8 @@ +use base64::Engine; use mudu::common::id::OID; use mudu_binding::universal::uni_oid::UniOid; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{Value, json}; type AppResult = Result; @@ -58,12 +59,63 @@ pub struct ServerTopology { pub workers: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PartitionRouteEntry { + #[serde( + serialize_with = "serialize_oid_as_unioid", + deserialize_with = "deserialize_oid_from_unioid" + )] + pub partition_id: OID, + #[serde( + serialize_with = "serialize_oid_as_unioid", + deserialize_with = "deserialize_oid_from_unioid" + )] + pub worker_id: OID, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PartitionRouteResponse { + pub routes: Vec, +} + pub async fn fetch_server_topology(http_addr: &str) -> AppResult { let response = get_http_json(http_addr, "/mudu/server/topology").await?; let data = extract_http_api_data(response)?; serde_json::from_value(data).map_err(|e| format!("decode server topology failed: {}", e)) } +pub fn is_server_topology_unsupported(err: &str) -> bool { + err.contains("server topology is not supported") + || err.contains("\"code\":\"NotImplemented\"") +} + +pub async fn install_app_package(http_addr: &str, mpk_binary: Vec) -> AppResult<()> { + let payload = json!({ + "mpk_base64": base64::engine::general_purpose::STANDARD.encode(mpk_binary), + }); + let response = post_http_json(http_addr, "/mudu/app/install", payload).await?; + let _ = extract_http_api_data(response)?; + Ok(()) +} + +pub async fn route_partition( + http_addr: &str, + rule_name: &str, + key: Option>, + start: Option>, + end: Option>, +) -> AppResult { + let payload = json!({ + "rule_name": rule_name, + "key": key, + "start": start, + "end": end, + }); + let response = post_http_json(http_addr, "/mudu/partition/route", payload).await?; + let data = extract_http_api_data(response)?; + serde_json::from_value(data).map_err(|e| format!("decode partition route failed: {}", e)) +} + async fn get_http_json(http_addr: &str, path: &str) -> AppResult { let url = format!("http://{}{}", http_addr, path); let client = reqwest::Client::builder() @@ -81,6 +133,24 @@ async fn get_http_json(http_addr: &str, path: &str) -> AppResult { .map_err(|e| format!("decode HTTP response from {} failed: {}", url, e)) } +async fn post_http_json(http_addr: &str, path: &str, payload: Value) -> AppResult { + let url = format!("http://{}{}", http_addr, path); + let client = reqwest::Client::builder() + .no_proxy() + .build() + .map_err(|e| format!("build HTTP client failed: {}", e))?; + let response = client + .post(&url) + .json(&payload) + .send() + .await + .map_err(|e| format!("POST {} failed: {}", url, e))?; + response + .json::() + .await + .map_err(|e| format!("decode HTTP response from {} failed: {}", url, e)) +} + fn extract_http_api_data(response: Value) -> AppResult { let status = response .get("status") @@ -146,4 +216,23 @@ mod tests { let decoded: WorkerTopology = serde_json::from_value(value).unwrap(); assert_eq!(decoded, worker); } + + #[tokio::test] + async fn install_app_package_rejects_http_failure() { + let err = install_app_package("127.0.0.1:1", vec![1, 2, 3]) + .await + .unwrap_err(); + assert!(err.contains("failed") || err.contains("error")); + } + + #[test] + fn detect_unsupported_topology_api_errors() { + assert!(is_server_topology_unsupported( + "{\"code\":\"NotImplemented\",\"msg\":\"server topology is not supported\"}" + )); + assert!(is_server_topology_unsupported( + "fail to get server topology: server topology is not supported" + )); + assert!(!is_server_topology_unsupported("connection refused")); + } } diff --git a/mudu_contract/src/database/err_no.rs b/mudu_contract/src/database/err_no.rs deleted file mode 100644 index 8f540f4..0000000 --- a/mudu_contract/src/database/err_no.rs +++ /dev/null @@ -1,28 +0,0 @@ -use lazy_static::lazy_static; -use std::collections::HashMap; - -pub const EN_OK: i32 = 0; -pub const EN_DECODE_PARAM: i32 = 1; -pub const EN_INVOKE: i32 = 2; -pub const EN_INSUFFICIENT_BUFFER_LENGTH_FOR_OUTPUT: i32 = 3; -pub const EN_NO_OUTPUT_MEMORY: i32 = 3; -pub const EN_ENCODE_RESULT: i32 = 4; - -lazy_static! { - static ref ERR_MSG: HashMap = HashMap::from([ - (EN_DECODE_PARAM, "encode parameter error"), - (EN_INVOKE, "invoke procedure error"), - ( - EN_INSUFFICIENT_BUFFER_LENGTH_FOR_OUTPUT, - "insufficient buffer length for output error" - ), - (EN_NO_OUTPUT_MEMORY, "memory error"), - (EN_ENCODE_RESULT, "encode result error"), - ]); -} - -pub fn errno_to_msg(errno: i32) -> String { - ERR_MSG - .get(&errno) - .map_or(format!("no such error number {}", errno), |s| s.to_string()) -} diff --git a/mudu_contract/src/database/filter.rs b/mudu_contract/src/database/filter.rs deleted file mode 100644 index 3d458bc..0000000 --- a/mudu_contract/src/database/filter.rs +++ /dev/null @@ -1,48 +0,0 @@ -use mudu_type::dat_typed::DatTyped; - -#[derive(Debug, Eq, PartialEq, Clone, Copy)] -pub enum OpType { - Equal, - Less, - Greater, - LessOrEqual, - GreaterOrEqual, -} - -pub struct Filter { - table_name: &'static str, - column_name: &'static str, - op_type: OpType, - datum: DatTyped, -} - -impl Filter { - pub fn new( - table_name: &'static str, - column_name: &'static str, - op_type: OpType, - datum: DatTyped, - ) -> Filter { - Self { - table_name, - column_name, - op_type, - datum, - } - } - pub fn op_type(&self) -> OpType { - self.op_type - } - - pub fn table_name(&self) -> &'static &str { - &self.table_name - } - - pub fn column_name(&self) -> &'static &str { - &self.column_name - } - - pub fn datum(&self) -> &DatTyped { - &self.datum - } -} diff --git a/mudu_contract/src/database/mod.rs b/mudu_contract/src/database/mod.rs index 0ba86f9..e5c66dd 100644 --- a/mudu_contract/src/database/mod.rs +++ b/mudu_contract/src/database/mod.rs @@ -3,18 +3,13 @@ pub mod attr_value; pub mod context; pub mod db_conn; pub mod entity; -pub mod filter; -pub mod predicate; -pub mod project; pub mod sql; pub mod sql_stmt; -pub mod table; pub mod attr_field_access; mod db_context; pub mod entity_set; pub mod entity_utils; -pub mod err_no; pub mod prepared_stmt; pub mod result_batch; pub mod result_set; diff --git a/mudu_contract/src/database/predicate.rs b/mudu_contract/src/database/predicate.rs deleted file mode 100644 index 8bde7d9..0000000 --- a/mudu_contract/src/database/predicate.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::database::filter::Filter; - -pub struct Predicate { - filter: Vec>, -} - -impl Predicate { - pub fn new(filter_and: Vec) -> Self { - let mut s = Self { filter: Vec::new() }; - s.filter.push(filter_and); - s - } - - pub fn or(&mut self, predicate: Predicate) -> &mut Self { - let mut filter = predicate.filter; - self.filter.append(&mut filter); - self - } -} diff --git a/mudu_contract/src/database/project.rs b/mudu_contract/src/database/project.rs deleted file mode 100644 index f16e23e..0000000 --- a/mudu_contract/src/database/project.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub struct Project { - table_name: &'static str, - column_name: &'static str, -} - -impl Project { - pub fn new(table_name: &'static str, column_name: &'static str) -> Project { - Self { - table_name, - column_name, - } - } - - pub fn table_name(&self) -> &'static str { - &self.table_name - } - - pub fn column_name(&self) -> &'static str { - &self.column_name - } -} diff --git a/mudu_contract/src/database/table.rs b/mudu_contract/src/database/table.rs deleted file mode 100644 index e69af9a..0000000 --- a/mudu_contract/src/database/table.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::database::context::Context; -use crate::database::entity::Entity; -use crate::database::predicate::Predicate; -use crate::database::project::Project; -use mudu::common::result::RS; -use std::marker::PhantomData; - -pub struct Iter { - phantom: PhantomData, -} - -pub trait Iterator { - type Item; - - fn next(&self) -> RS> { - unimplemented!() - } -} -impl Iter { - pub fn new() -> Self { - Self { - phantom: Default::default(), - } - } -} - -impl Iterator for Iter { - type Item = R; - - fn next(&self) -> RS> { - unimplemented!() - } -} - -pub trait Table { - fn table_name() -> &'static str; - - fn query(&self, context: &Context, predicate: &Predicate, project: &Project) -> RS>; - - fn insert(&self, context: &Context, tuple: R) -> RS<()>; - - fn update(&self, context: &Context, tuple: R, key_predicate: &Predicate) -> RS<()>; - - fn delete(&self, context: &Context, key_predicate: &Predicate) -> RS<()>; -} diff --git a/mudu_contract/src/protocol.rs b/mudu_contract/src/protocol.rs index bacab2e..01dd6a6 100644 --- a/mudu_contract/src/protocol.rs +++ b/mudu_contract/src/protocol.rs @@ -1,3 +1,5 @@ +use crate::tuple::tuple_field_desc::TupleFieldDesc; +use crate::tuple::tuple_value::TupleValue; use mudu::common::result::RS; use mudu::error::ec::EC; use mudu::m_error; @@ -75,8 +77,8 @@ pub struct ClientRequest { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerResponse { - columns: Vec, - rows: Vec>, + row_desc: TupleFieldDesc, + rows: Vec, affected_rows: u64, error: Option, } @@ -319,24 +321,24 @@ impl ClientRequest { impl ServerResponse { pub fn new( - columns: Vec, - rows: Vec>, + row_desc: TupleFieldDesc, + rows: Vec, affected_rows: u64, error: Option, ) -> Self { Self { - columns, + row_desc, rows, affected_rows, error, } } - pub fn columns(&self) -> &[String] { - &self.columns + pub fn row_desc(&self) -> &TupleFieldDesc { + &self.row_desc } - pub fn rows(&self) -> &[Vec] { + pub fn rows(&self) -> &[TupleValue] { &self.rows } @@ -853,17 +855,27 @@ mod tests { b"input".to_vec() ); + use crate::tuple::datum_desc::DatumDesc; + use crate::tuple::tuple_field_desc::TupleFieldDesc; + use crate::tuple::tuple_value::TupleValue; + use mudu_type::dat_type::DatType; + use mudu_type::dat_type_id::DatTypeID; + use mudu_type::dat_value::DatValue; + let response = ServerResponse::new( - vec!["value".to_string()], - vec![vec!["1".to_string()]], + TupleFieldDesc::new(vec![DatumDesc::new( + "value".to_string(), + DatType::default_for(DatTypeID::String), + )]), + vec![TupleValue::from(vec![DatValue::from_string("1".to_string())])], 0, None, ); let response_frame = Frame::decode(&encode_server_response(12, &response).unwrap()).unwrap(); let decoded_response = decode_server_response(&response_frame).unwrap(); - assert_eq!(decoded_response.columns(), &["value".to_string()]); - assert_eq!(decoded_response.rows(), &[vec!["1".to_string()]]); + assert_eq!(decoded_response.row_desc().fields()[0].name(), "value"); + assert_eq!(decoded_response.rows()[0].values()[0].expect_string(), "1"); let get_response_frame = Frame::decode( &encode_get_response(13, &GetResponse::new(Some(b"v".to_vec()))).unwrap(), diff --git a/mudu_contract/src/tuple/build_tuple.rs b/mudu_contract/src/tuple/build_tuple.rs index 56b4c8e..f167855 100644 --- a/mudu_contract/src/tuple/build_tuple.rs +++ b/mudu_contract/src/tuple/build_tuple.rs @@ -16,7 +16,7 @@ pub fn build_tuple_into( return Ok(Err(tuple_desc.min_tuple_size())); } let mut offset = tuple_desc.meta_size(); - assert!(offset < tuple.len()); + assert!(offset <= tuple.len()); for (i, v) in vec.iter().enumerate() { let field = tuple_desc.get_field_desc(i); let r = write_value::write_value_to_tuple(field, offset, v, tuple)?; @@ -42,7 +42,7 @@ pub fn build_tuple(vec: &Vec, tuple_desc: &TupleBinaryDesc) -> RS, tuple_desc: &TupleBinaryDesc) -> RS RS<&[u8]> { Err(m_error!(EC::IndexOutOfRange)) } else { let slot = Slot::from_binary(&tuple[_offset.._offset + Slot::size_of()]); - if tuple.len() <= slot.offset() + slot.length() { + if tuple.len() < slot.offset() + slot.length() { return Err(m_error!(EC::IndexOutOfRange)); } Ok(&tuple[slot.offset()..slot.offset() + slot.length()]) diff --git a/mudu_contract/src/tuple/tuple_binary_desc.rs b/mudu_contract/src/tuple/tuple_binary_desc.rs index 057fc15..4877f98 100644 --- a/mudu_contract/src/tuple/tuple_binary_desc.rs +++ b/mudu_contract/src/tuple/tuple_binary_desc.rs @@ -46,9 +46,8 @@ impl TupleBinaryDesc { } let offset_hdr = 0; let offset_slot_begin = offset_hdr; - let mut offset_slot_var = - (offset_slot_begin + total_fixed_size + var_count * Slot::size_of()) as u32; - let mut offset_data_fixed = offset_slot_begin as u32; + let mut offset_slot_var = offset_slot_begin as u32; + let mut offset_data_fixed = (offset_slot_begin + var_count * Slot::size_of()) as u32; let mut offset_len_data_fixed: Vec = vec![]; let mut offset_len_slot_var: Vec = vec![]; let mut slot_all: Vec = vec![]; @@ -63,10 +62,10 @@ impl TupleBinaryDesc { offset_data_fixed += data_len; } None => { - offset_slot_var += Slot::size_of() as u32; let slot = Slot::new(offset_slot_var, Slot::size_of() as u32); slot_all.push(FieldDesc::new(slot.clone(), ty.clone(), false)); offset_len_slot_var.push(FieldDesc::new(slot, ty.clone(), false)); + offset_slot_var += Slot::size_of() as u32; } }, Err(e) => { diff --git a/mudu_contract/src/tuple/tuple_value.rs b/mudu_contract/src/tuple/tuple_value.rs index 31b403a..adc16ec 100644 --- a/mudu_contract/src/tuple/tuple_value.rs +++ b/mudu_contract/src/tuple/tuple_value.rs @@ -1,5 +1,7 @@ +use serde::{Deserialize, Serialize}; use mudu_type::dat_value::DatValue; +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct TupleValue { value: Vec, } diff --git a/mudu_contract/src/tuple/write_value.rs b/mudu_contract/src/tuple/write_value.rs index 191b6ea..23db4e7 100644 --- a/mudu_contract/src/tuple/write_value.rs +++ b/mudu_contract/src/tuple/write_value.rs @@ -4,12 +4,11 @@ use crate::tuple::tuple_binary::TupleSlice; use mudu::common::buf::Buf; use mudu::common::result::RS; use mudu::error::ec::EC; -use mudu::error::err::MError; use mudu::m_error; pub fn write_slot_to_buf(value_offset: usize, value_size: usize, buf: &mut [u8]) -> RS<()> { let slot = Slot::new(value_offset as u32, value_size as u32); - if Slot::size_of() < buf.len() { + if Slot::size_of() > buf.len() { return Err(m_error!(EC::NotImplemented)); } slot.to_binary(buf); @@ -41,19 +40,11 @@ pub fn write_value_to_buf( value: &Buf, buf: &mut [u8], ) -> RS> { - let r = { - if value.len() > buf.len() { - return Err(m_error!(EC::InternalErr, "buffer size error ")); - } - buf[0..value.len()].copy_from_slice(value); - Ok::<_, MError>(value.len()) - }; - - let len = match r { - Ok(n) => n, - Err(e) => return Err(e), - }; - Ok(Ok(len)) + if value.len() > buf.len() { + return Ok(Err(value.len())); + } + buf[0..value.len()].copy_from_slice(value); + Ok(Ok(value.len())) } pub fn write_value_to_tuple( diff --git a/mudu_kernel/src/command/create_partition_placement.rs b/mudu_kernel/src/command/create_partition_placement.rs new file mode 100644 index 0000000..8308bbd --- /dev/null +++ b/mudu_kernel/src/command/create_partition_placement.rs @@ -0,0 +1,44 @@ +use crate::contract::cmd_exec::CmdExec; +use crate::contract::meta_mgr::MetaMgr; +use crate::x_engine::x_param::PCreatePartitionPlacement; +use async_trait::async_trait; +use mudu::common::result::RS; +use mudu_utils::sync::a_mutex::AMutex; +use mudu_utils::task_trace; +use std::sync::Arc; + +pub struct CreatePartitionPlacement { + inner: AMutex, +} + +struct InnerCreatePartitionPlacement { + param: PCreatePartitionPlacement, + meta_mgr: Arc, +} + +impl CreatePartitionPlacement { + pub fn new(param: PCreatePartitionPlacement, meta_mgr: Arc) -> Self { + Self { + inner: AMutex::new(InnerCreatePartitionPlacement { param, meta_mgr }), + } + } +} + +#[async_trait] +impl CmdExec for CreatePartitionPlacement { + async fn prepare(&self) -> RS<()> { + Ok(()) + } + + async fn run(&self) -> RS<()> { + task_trace!(); + let inner = self.inner.lock().await; + inner.meta_mgr + .upsert_partition_placements(&inner.param.placements) + .await + } + + async fn affected_rows(&self) -> RS { + Ok(0) + } +} diff --git a/mudu_kernel/src/command/create_partition_rule.rs b/mudu_kernel/src/command/create_partition_rule.rs new file mode 100644 index 0000000..1a3616f --- /dev/null +++ b/mudu_kernel/src/command/create_partition_rule.rs @@ -0,0 +1,66 @@ +use crate::contract::cmd_exec::CmdExec; +use crate::contract::meta_mgr::MetaMgr; +use crate::x_engine::x_param::PCreatePartitionRule; +use async_trait::async_trait; +use mudu::common::result::RS; +use mudu::error::ec::EC as ER; +use mudu::m_error; +use mudu_utils::sync::a_mutex::AMutex; +use mudu_utils::task_trace; +use std::sync::Arc; + +pub struct CreatePartitionRule { + inner: AMutex, +} + +struct InnerCreatePartitionRule { + param: PCreatePartitionRule, + meta_mgr: Arc, +} + +impl CreatePartitionRule { + pub fn new(param: PCreatePartitionRule, meta_mgr: Arc) -> Self { + Self { + inner: AMutex::new(InnerCreatePartitionRule { param, meta_mgr }), + } + } +} + +#[async_trait] +impl CmdExec for CreatePartitionRule { + async fn prepare(&self) -> RS<()> { + let inner = self.inner.lock().await; + inner.prepare().await + } + + async fn run(&self) -> RS<()> { + task_trace!(); + let inner = self.inner.lock().await; + inner.run().await + } + + async fn affected_rows(&self) -> RS { + Ok(0) + } +} + +impl InnerCreatePartitionRule { + async fn prepare(&self) -> RS<()> { + if self + .meta_mgr + .get_partition_rule_by_name(&self.param.rule.name) + .await? + .is_some() + { + return Err(m_error!( + ER::ExistingSuchElement, + format!("partition rule {} already exists", self.param.rule.name) + )); + } + Ok(()) + } + + async fn run(&self) -> RS<()> { + self.meta_mgr.create_partition_rule(&self.param.rule).await + } +} diff --git a/mudu_kernel/src/command/create_table.rs b/mudu_kernel/src/command/create_table.rs index ab15442..9c44a71 100644 --- a/mudu_kernel/src/command/create_table.rs +++ b/mudu_kernel/src/command/create_table.rs @@ -85,6 +85,10 @@ impl _InnerCreateTable { task_trace!(); self.x_contract .create_table(self.param.tx_mgr.clone(), &self.param.schema) - .await + .await?; + if let Some(binding) = &self.param.partition_binding { + self.meta_mgr.bind_table_partition(binding).await?; + } + Ok(()) } } diff --git a/mudu_kernel/src/command/mod.rs b/mudu_kernel/src/command/mod.rs index 9e7c0b1..18b8f35 100644 --- a/mudu_kernel/src/command/mod.rs +++ b/mudu_kernel/src/command/mod.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] +pub mod create_partition_placement; +pub mod create_partition_rule; pub mod create_table; pub mod delete_key_value; pub mod drop_table; diff --git a/mudu_kernel/src/contract/mem_store.rs b/mudu_kernel/src/contract/mem_store.rs deleted file mode 100644 index d82e027..0000000 --- a/mudu_kernel/src/contract/mem_store.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::contract::data_row::DataRow; -use async_trait::async_trait; -use mudu::common::buf::Buf; -use mudu::common::id::OID; -use mudu::common::result::RS; -use mudu_contract::tuple::tuple_binary_desc::TupleBinaryDesc as TupleDesc; -use std::ops::Bound; - -#[async_trait] -pub trait MemStore: Send + Sync { - async fn create_table(&self, oid: OID, key_desc: TupleDesc) -> RS<()>; - - async fn drop_table(&self, oid: OID) -> RS<()>; - - async fn get_key(&self, oid: OID, key: Buf) -> RS>; - - async fn read_range(&self, oid: OID, begin: Bound, end: Bound) -> RS>; - - async fn insert_key(&self, oid: OID, key: Buf, row: DataRow) -> RS>; -} diff --git a/mudu_kernel/src/contract/meta_mgr.rs b/mudu_kernel/src/contract/meta_mgr.rs index eb43be6..ad95a07 100644 --- a/mudu_kernel/src/contract/meta_mgr.rs +++ b/mudu_kernel/src/contract/meta_mgr.rs @@ -1,9 +1,12 @@ use async_trait::async_trait; use mudu::common::id::OID; +use mudu::error::ec::EC; use std::sync::Arc; use crate::contract::schema_table::SchemaTable; use crate::contract::table_desc::TableDesc; +use crate::contract::partition_rule::PartitionRuleDesc; +use crate::contract::partition_rule_binding::{PartitionPlacement, TablePartitionBinding}; use mudu::common::result::RS; #[async_trait] @@ -16,6 +19,57 @@ pub trait MetaMgr: Send + Sync { async fn drop_table(&self, table_id: OID) -> RS<()>; + async fn create_partition_rule(&self, _rule: &PartitionRuleDesc) -> RS<()> { + Err(mudu::m_error!( + EC::NotImplemented, + "partition rule catalog is not implemented" + )) + } + + async fn get_partition_rule_by_id(&self, oid: OID) -> RS { + Err(mudu::m_error!( + EC::NoSuchElement, + format!("no such partition rule {}", oid) + )) + } + + async fn get_partition_rule_by_name(&self, _name: &str) -> RS> { + Ok(None) + } + + async fn list_partition_rules(&self) -> RS> { + Ok(Vec::new()) + } + + async fn bind_table_partition(&self, _binding: &TablePartitionBinding) -> RS<()> { + Err(mudu::m_error!( + EC::NotImplemented, + "table partition binding is not implemented" + )) + } + + async fn get_table_partition_binding( + &self, + _table_id: OID, + ) -> RS> { + Ok(None) + } + + async fn upsert_partition_placements(&self, _placements: &[PartitionPlacement]) -> RS<()> { + Err(mudu::m_error!( + EC::NotImplemented, + "partition placement is not implemented" + )) + } + + async fn get_partition_worker(&self, _partition_id: OID) -> RS> { + Ok(None) + } + + async fn list_partition_placements(&self) -> RS> { + Ok(Vec::new()) + } + async fn list_schemas(&self) -> RS> { Ok(Vec::new()) } diff --git a/mudu_kernel/src/contract/mod.rs b/mudu_kernel/src/contract/mod.rs index 442c2b6..fd74283 100644 --- a/mudu_kernel/src/contract/mod.rs +++ b/mudu_kernel/src/contract/mod.rs @@ -1,16 +1,15 @@ #![allow(dead_code)] pub mod lsn; -pub mod mem_store; pub mod x_lock_mgr; pub mod meta_mgr; +pub mod partition_rule; +pub mod partition_rule_binding; pub mod cmd_exec; pub mod data_row; mod field_info; -pub mod pst_op; -pub mod pst_op_list; pub mod query_exec; pub mod schema_column; pub mod schema_table; @@ -23,5 +22,5 @@ pub mod timestamp; pub mod version_delta; pub mod version_tuple; pub mod waiter; -pub mod xl_d_up_tuple; mod worker_snapshot; +pub mod xl_d_up_tuple; diff --git a/mudu_kernel/src/contract/partition_rule.rs b/mudu_kernel/src/contract/partition_rule.rs new file mode 100644 index 0000000..9c178d1 --- /dev/null +++ b/mudu_kernel/src/contract/partition_rule.rs @@ -0,0 +1,64 @@ +use mudu::common::id::{gen_oid, OID}; +use mudu_type::dat_type_id::DatTypeID; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum PartitionRuleKind { + Range, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum PartitionBound { + Unbounded, + Value(Vec>), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct RangePartitionDef { + pub partition_id: OID, + pub name: String, + pub start: PartitionBound, + pub end: PartitionBound, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct PartitionRuleDesc { + pub oid: OID, + pub name: String, + pub kind: PartitionRuleKind, + pub key_types: Vec, + pub partitions: Vec, + pub version: u64, +} + +impl RangePartitionDef { + pub fn new( + name: String, + start: PartitionBound, + end: PartitionBound, + ) -> Self { + Self { + partition_id: gen_oid(), + name, + start, + end, + } + } +} + +impl PartitionRuleDesc { + pub fn new_range( + name: String, + key_types: Vec, + partitions: Vec, + ) -> Self { + Self { + oid: gen_oid(), + name, + kind: PartitionRuleKind::Range, + key_types, + partitions, + version: 1, + } + } +} diff --git a/mudu_kernel/src/contract/partition_rule_binding.rs b/mudu_kernel/src/contract/partition_rule_binding.rs new file mode 100644 index 0000000..51b41f5 --- /dev/null +++ b/mudu_kernel/src/contract/partition_rule_binding.rs @@ -0,0 +1,15 @@ +use mudu::common::id::{AttrIndex, OID}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct TablePartitionBinding { + pub table_id: OID, + pub rule_id: OID, + pub ref_attr_indices: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct PartitionPlacement { + pub partition_id: OID, + pub worker_id: OID, +} diff --git a/mudu_kernel/src/contract/pst_op.rs b/mudu_kernel/src/contract/pst_op.rs deleted file mode 100644 index 3c6da15..0000000 --- a/mudu_kernel/src/contract/pst_op.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::contract::timestamp::Timestamp; -use mudu::common::buf::Buf; -use mudu::common::id::OID; -use tokio::sync::oneshot::Sender; - -pub struct InsertKV { - pub table_id: OID, - pub tuple_id: OID, - pub timestamp: Timestamp, - pub key: Buf, - pub value: Buf, -} - -pub struct UpdateV { - pub table_id: OID, - pub tuple_id: OID, - pub timestamp: Timestamp, - pub value: Buf, -} - -pub struct DeleteKV { - pub table_id: OID, - pub tuple_id: OID, -} - -pub struct WriteDelta { - pub table_id: OID, - pub tuple_id: OID, - pub timestamp: Timestamp, - pub delta: Buf, -} - -pub enum PstOp { - InsertKV(InsertKV), - UpdateV(UpdateV), - DeleteKV(DeleteKV), - WriteDelta(WriteDelta), - Flush(Sender<()>), - Stop(Sender<()>), -} diff --git a/mudu_kernel/src/contract/pst_op_list.rs b/mudu_kernel/src/contract/pst_op_list.rs deleted file mode 100644 index e6ed6ff..0000000 --- a/mudu_kernel/src/contract/pst_op_list.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::contract::pst_op::{DeleteKV, InsertKV, PstOp, UpdateV}; -use crate::contract::timestamp::Timestamp; -use mudu::common::buf::Buf; -use mudu::common::id::OID; -use tokio::sync::oneshot::Sender; - -impl PstOpList { - pub fn new() -> PstOpList { - Self { ops: Vec::new() } - } - - pub fn into_ops(self) -> Vec { - self.ops - } - - pub fn push_delete(&mut self, table_id: OID, tuple_id: OID) { - self.ops - .push(PstOp::DeleteKV(DeleteKV { table_id, tuple_id })); - } - - pub fn push_insert( - &mut self, - table_id: OID, - tuple_id: OID, - timestamp: Timestamp, - key: Buf, - value: Buf, - ) { - let op = InsertKV { - table_id, - tuple_id, - timestamp, - key, - value, - }; - self.ops.push(PstOp::InsertKV(op)) - } - - pub fn push_update(&mut self, table_id: OID, tuple_id: OID, timestamp: Timestamp, value: Buf) { - let op = UpdateV { - table_id, - tuple_id, - timestamp, - value, - }; - self.ops.push(PstOp::UpdateV(op)) - } - - pub fn push_stop(&mut self, sender: Sender<()>) { - self.ops.push(PstOp::Stop(sender)) - } - - pub fn push_flush(&mut self, sender: Sender<()>) { - self.ops.push(PstOp::Flush(sender)) - } -} - -pub struct PstOpList { - ops: Vec, -} diff --git a/mudu_kernel/src/io/socket.rs b/mudu_kernel/src/io/socket.rs index 20f5a23..95fbf32 100644 --- a/mudu_kernel/src/io/socket.rs +++ b/mudu_kernel/src/io/socket.rs @@ -1003,7 +1003,7 @@ mod tests { assert_eq!(sock.fd(), 41); let connect_task = - tokio::spawn(async move { connect(&sock, "127.0.0.1:9000".parse().unwrap()).await }); + tokio::spawn(async move { connect(&sock, "127.0.0.1:9527".parse().unwrap()).await }); yield_now().await; match ring.take_pending().unwrap().unwrap().1 { WorkerRingOp::Socket(SocketIoRequest::Connect(request)) => { diff --git a/mudu_kernel/src/lib.rs b/mudu_kernel/src/lib.rs index 9fae0b8..e3f24af 100644 --- a/mudu_kernel/src/lib.rs +++ b/mudu_kernel/src/lib.rs @@ -3,7 +3,7 @@ pub mod contract; pub mod fuzz; pub mod index; pub mod io; -mod meta; +pub mod meta; pub mod mudu_conn; pub mod sql; pub mod wal; diff --git a/mudu_kernel/src/meta/meta_mgr.rs b/mudu_kernel/src/meta/meta_mgr.rs index c9f389f..0e479a5 100644 --- a/mudu_kernel/src/meta/meta_mgr.rs +++ b/mudu_kernel/src/meta/meta_mgr.rs @@ -12,9 +12,22 @@ use mudu::error::ec::EC as ER; use mudu::m_error; use crate::contract::meta_mgr::MetaMgr; +use crate::contract::partition_rule::PartitionRuleDesc; +use crate::contract::partition_rule_binding::{PartitionPlacement, TablePartitionBinding}; use crate::contract::schema_table::SchemaTable; use crate::contract::table_desc::TableDesc; use crate::contract::table_info::TableInfo; +use crate::meta::partition_binding_catalog::{ + load_partition_bindings_from_catalog, open_partition_binding_catalog, + write_partition_binding_to_catalog, +}; +use crate::meta::partition_placement_catalog::{ + load_partition_placements_from_catalog, open_partition_placement_catalog, + write_partition_placement_to_catalog, +}; +use crate::meta::partition_rule_catalog::{ + load_partition_rules_from_catalog, open_partition_rule_catalog, write_partition_rule_to_catalog, +}; use crate::meta::schema_catalog::{ delete_schema_from_catalog, load_schemas_from_catalog, open_schema_catalog, write_schema_to_catalog, @@ -36,10 +49,17 @@ fn ddl_lock() -> &'static tokio::sync::Mutex<()> { pub struct MetaMgrImpl { path: String, schema_catalog: Relation, + partition_rule_catalog: Relation, + partition_binding_catalog: Relation, + partition_placement_catalog: Relation, next_catalog_xid: AtomicU64, id2table: scc::HashMap, name2id: scc::HashMap, table: scc::HashMap, + rule_by_id: scc::HashMap, + rule_name2id: scc::HashMap, + binding_by_table_id: scc::HashMap, + placement_by_partition_id: scc::HashMap, } impl MetaMgrImpl { @@ -51,17 +71,36 @@ impl MetaMgrImpl { let path_string = path.to_string_lossy().to_string(); let schema_catalog = open_schema_catalog(&path_string)?; + let partition_rule_catalog = open_partition_rule_catalog(&path_string)?; + let partition_binding_catalog = open_partition_binding_catalog(&path_string)?; + let partition_placement_catalog = open_partition_placement_catalog(&path_string)?; let this = Self { path: path_string, schema_catalog, + partition_rule_catalog, + partition_binding_catalog, + partition_placement_catalog, next_catalog_xid: AtomicU64::new(now_catalog_xid()), id2table: Default::default(), name2id: Default::default(), table: Default::default(), + rule_by_id: Default::default(), + rule_name2id: Default::default(), + binding_by_table_id: Default::default(), + placement_by_partition_id: Default::default(), }; for schema in load_schemas_from_catalog(&this.schema_catalog)? { this.apply_create_table_local(&schema)?; } + for rule in load_partition_rules_from_catalog(&this.partition_rule_catalog)? { + this.apply_create_partition_rule_local(&rule); + } + for binding in load_partition_bindings_from_catalog(&this.partition_binding_catalog)? { + this.apply_bind_table_partition_local(&binding); + } + for placement in load_partition_placements_from_catalog(&this.partition_placement_catalog)? { + this.apply_partition_placement_local(&placement); + } Ok(this) } @@ -97,10 +136,47 @@ impl MetaMgrImpl { schemas } + pub fn lookup_partition_rule_by_id(&self, oid: OID) -> Option { + self.rule_by_id.get_sync(&oid).map(|entry| entry.get().clone()) + } + + pub fn lookup_partition_rule_by_name(&self, name: &str) -> Option { + let rule_id = self.rule_name2id.get_sync(name).map(|entry| *entry.get())?; + self.lookup_partition_rule_by_id(rule_id) + } + + pub fn list_partition_rules_inner(&self) -> Vec { + let mut rules = Vec::new(); + self.rule_by_id.iter_sync(|_rule_id, rule| { + rules.push(rule.clone()); + true + }); + rules.sort_by_key(|rule| rule.oid); + rules + } + + pub fn lookup_table_partition_binding(&self, table_id: OID) -> Option { + self.binding_by_table_id + .get_sync(&table_id) + .map(|entry| entry.get().clone()) + } + + pub fn list_partition_placements_inner(&self) -> Vec { + let mut placements = Vec::new(); + self.placement_by_partition_id + .iter_sync(|partition_id, worker_id| { + placements.push(PartitionPlacement { + partition_id: *partition_id, + worker_id: *worker_id, + }); + true + }); + placements.sort_by_key(|placement| placement.partition_id); + placements + } + pub async fn create_table_inner(&self, schema: &SchemaTable) -> RS<()> { - let _ddl_guard = ddl_lock() - .lock() - .await; + let _ddl_guard = ddl_lock().lock().await; if self.table.contains_sync(schema.table_name()) { return Err(m_error!(ER::ExistingSuchElement, "")); } @@ -110,9 +186,7 @@ impl MetaMgrImpl { } pub async fn drop_table_inner(&self, oid: OID) -> RS<()> { - let _ddl_guard = ddl_lock() - .lock() - .await; + let _ddl_guard = ddl_lock().lock().await; let table = self .lookup_table_info_by_id(oid) .ok_or_else(|| m_error!(ER::NoSuchElement, format!("no such table {}", oid)))?; @@ -121,6 +195,62 @@ impl MetaMgrImpl { self.broadcast_drop(table.schema().table_name(), oid) } + pub async fn create_partition_rule_inner(&self, rule: &PartitionRuleDesc) -> RS<()> { + let _ddl_guard = ddl_lock().lock().await; + if self.rule_name2id.contains_sync(&rule.name) { + return Err(m_error!( + ER::ExistingSuchElement, + format!("partition rule {} already exists", rule.name) + )); + } + write_partition_rule_to_catalog( + &self.partition_rule_catalog, + rule, + self.next_catalog_xid(), + ) + .await?; + self.broadcast_create_partition_rule(rule) + } + + pub async fn bind_table_partition_inner(&self, binding: &TablePartitionBinding) -> RS<()> { + let _ddl_guard = ddl_lock().lock().await; + if self.lookup_table_info_by_id(binding.table_id).is_none() { + return Err(m_error!( + ER::NoSuchElement, + format!("no such table {}", binding.table_id) + )); + } + if self.lookup_partition_rule_by_id(binding.rule_id).is_none() { + return Err(m_error!( + ER::NoSuchElement, + format!("no such partition rule {}", binding.rule_id) + )); + } + write_partition_binding_to_catalog( + &self.partition_binding_catalog, + binding, + self.next_catalog_xid(), + ) + .await?; + self.broadcast_bind_table_partition(binding) + } + + pub async fn upsert_partition_placements_inner( + &self, + placements: &[PartitionPlacement], + ) -> RS<()> { + let _ddl_guard = ddl_lock().lock().await; + for placement in placements { + write_partition_placement_to_catalog( + &self.partition_placement_catalog, + placement, + self.next_catalog_xid(), + ) + .await?; + } + self.broadcast_upsert_partition_placements(placements) + } + fn next_catalog_xid(&self) -> u64 { let mut next = self.next_catalog_xid.load(Ordering::Relaxed); loop { @@ -153,6 +283,23 @@ impl MetaMgrImpl { let _ = self.table.remove_sync(table_name); } + fn apply_create_partition_rule_local(&self, rule: &PartitionRuleDesc) { + let _ = self.rule_name2id.insert_sync(rule.name.clone(), rule.oid); + let _ = self.rule_by_id.insert_sync(rule.oid, rule.clone()); + } + + fn apply_bind_table_partition_local(&self, binding: &TablePartitionBinding) { + let _ = self + .binding_by_table_id + .insert_sync(binding.table_id, binding.clone()); + } + + fn apply_partition_placement_local(&self, placement: &PartitionPlacement) { + let _ = self + .placement_by_partition_id + .insert_sync(placement.partition_id, placement.worker_id); + } + fn broadcast_create(&self, schema: &SchemaTable) -> RS<()> { let peers = self.peer_instances(); if peers.is_empty() { @@ -176,6 +323,46 @@ impl MetaMgrImpl { Ok(()) } + fn broadcast_create_partition_rule(&self, rule: &PartitionRuleDesc) -> RS<()> { + let peers = self.peer_instances(); + if peers.is_empty() { + self.apply_create_partition_rule_local(rule); + return Ok(()); + } + for mgr in peers { + mgr.apply_create_partition_rule_local(rule); + } + Ok(()) + } + + fn broadcast_bind_table_partition(&self, binding: &TablePartitionBinding) -> RS<()> { + let peers = self.peer_instances(); + if peers.is_empty() { + self.apply_bind_table_partition_local(binding); + return Ok(()); + } + for mgr in peers { + mgr.apply_bind_table_partition_local(binding); + } + Ok(()) + } + + fn broadcast_upsert_partition_placements(&self, placements: &[PartitionPlacement]) -> RS<()> { + let peers = self.peer_instances(); + if peers.is_empty() { + for placement in placements { + self.apply_partition_placement_local(placement); + } + return Ok(()); + } + for mgr in peers { + for placement in placements { + mgr.apply_partition_placement_local(placement); + } + } + Ok(()) + } + fn peer_instances(&self) -> Vec> { let mut guard = registry().lock().unwrap(); let peers = guard.entry(self.path.clone()).or_default(); @@ -224,6 +411,50 @@ impl MetaMgr for MetaMgrImpl { self.drop_table_inner(table_id).await } + async fn create_partition_rule(&self, rule: &PartitionRuleDesc) -> RS<()> { + self.create_partition_rule_inner(rule).await + } + + async fn get_partition_rule_by_id(&self, oid: OID) -> RS { + self.lookup_partition_rule_by_id(oid).ok_or_else(|| { + m_error!(ER::NoSuchElement, format!("no such partition rule {}", oid)) + }) + } + + async fn get_partition_rule_by_name(&self, name: &str) -> RS> { + Ok(self.lookup_partition_rule_by_name(name)) + } + + async fn list_partition_rules(&self) -> RS> { + Ok(self.list_partition_rules_inner()) + } + + async fn bind_table_partition(&self, binding: &TablePartitionBinding) -> RS<()> { + self.bind_table_partition_inner(binding).await + } + + async fn get_table_partition_binding( + &self, + table_id: OID, + ) -> RS> { + Ok(self.lookup_table_partition_binding(table_id)) + } + + async fn upsert_partition_placements(&self, placements: &[PartitionPlacement]) -> RS<()> { + self.upsert_partition_placements_inner(placements).await + } + + async fn get_partition_worker(&self, partition_id: OID) -> RS> { + Ok(self + .placement_by_partition_id + .get_sync(&partition_id) + .map(|entry| *entry.get())) + } + + async fn list_partition_placements(&self) -> RS> { + Ok(self.list_partition_placements_inner()) + } + async fn list_schemas(&self) -> RS> { Ok(self.list_schemas_inner()) } diff --git a/mudu_kernel/src/meta/mod.rs b/mudu_kernel/src/meta/mod.rs index 7fd0105..720640f 100644 --- a/mudu_kernel/src/meta/mod.rs +++ b/mudu_kernel/src/meta/mod.rs @@ -4,4 +4,7 @@ pub mod _fuzz; pub mod meta_mgr; pub mod meta_mgr_factory; +pub mod partition_binding_catalog; +pub mod partition_placement_catalog; +pub mod partition_rule_catalog; pub mod schema_catalog; diff --git a/mudu_kernel/src/meta/partition_binding_catalog.rs b/mudu_kernel/src/meta/partition_binding_catalog.rs new file mode 100644 index 0000000..36a4f8e --- /dev/null +++ b/mudu_kernel/src/meta/partition_binding_catalog.rs @@ -0,0 +1,133 @@ +use std::ops::Bound; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use mudu::common::endian; +use mudu::common::id::OID; +use mudu::common::result::RS; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dt_info::DTInfo; + +use crate::contract::partition_rule_binding::TablePartitionBinding; +use crate::contract::schema_column::SchemaColumn; +use crate::contract::schema_table::SchemaTable; +use crate::contract::table_desc::TableDesc; +use crate::contract::table_info::TableInfo; +use crate::server::worker_snapshot::WorkerSnapshot; +use crate::storage::relation::relation::Relation; + +pub const PARTITION_BINDING_CATALOG_PARTITION_ID: OID = 0; +pub const PARTITION_BINDING_CATALOG_TABLE_ID: OID = 0x3; +const PARTITION_BINDING_CATALOG_TABLE_NAME: &str = "__meta_table_partition_binding"; +const PARTITION_BINDING_CATALOG_TABLE_OID_COLUMN_ID: OID = 0x30001; +const PARTITION_BINDING_CATALOG_BINDING_COLUMN_ID: OID = 0x30002; + +pub fn partition_binding_catalog_schema() -> SchemaTable { + SchemaTable::new_with_oid( + PARTITION_BINDING_CATALOG_TABLE_ID, + PARTITION_BINDING_CATALOG_TABLE_NAME.to_string(), + vec![ + SchemaColumn::new_with_oid( + PARTITION_BINDING_CATALOG_TABLE_OID_COLUMN_ID, + "table_oid".to_string(), + DatTypeID::U128, + DTInfo::from_text(DatTypeID::U128, String::new()), + ), + SchemaColumn::new_with_oid( + PARTITION_BINDING_CATALOG_BINDING_COLUMN_ID, + "binding".to_string(), + DatTypeID::Binary, + DTInfo::from_text(DatTypeID::Binary, String::new()), + ), + ], + vec![0], + vec![1], + ) +} + +pub fn partition_binding_catalog_desc() -> RS> { + TableInfo::new(partition_binding_catalog_schema())?.table_desc() +} + +pub fn open_partition_binding_catalog(path: &str) -> RS { + let desc = partition_binding_catalog_desc()?; + Ok(Relation::new( + PARTITION_BINDING_CATALOG_TABLE_ID, + PARTITION_BINDING_CATALOG_PARTITION_ID, + path.to_string(), + desc.as_ref(), + )) +} + +pub fn encode_partition_binding_catalog_key(oid: OID) -> RS> { + let mut key = vec![0; std::mem::size_of::()]; + endian::write_u128(&mut key, oid); + Ok(key) +} + +pub fn encode_partition_binding_catalog_value(binding: &TablePartitionBinding) -> RS> { + rmp_serde::to_vec(binding).map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::EncodeErr, + "encode partition binding catalog value error", + e + ) + }) +} + +pub fn decode_partition_binding_catalog_key(tuple: &[u8]) -> RS { + Ok(endian::read_u128(tuple)) +} + +pub fn decode_partition_binding_catalog_value(tuple: &[u8]) -> RS { + rmp_serde::from_slice(tuple).map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::DecodeErr, + "decode partition binding catalog value error", + e + ) + }) +} + +pub fn load_partition_bindings_from_catalog(relation: &Relation) -> RS> { + let rows = relation.visible_range_sync( + (Bound::Unbounded, Bound::Unbounded), + &WorkerSnapshot::new(visible_snapshot_xid(), vec![]), + )?; + let mut bindings = Vec::with_capacity(rows.len()); + for (key, value) in rows { + let key_oid = decode_partition_binding_catalog_key(&key)?; + let binding = decode_partition_binding_catalog_value(&value)?; + if key_oid != binding.table_id { + return Err(mudu::m_error!( + mudu::error::ec::EC::DecodeErr, + format!( + "partition binding catalog key oid {} does not match table oid {}", + key_oid, + binding.table_id + ) + )); + } + bindings.push(binding); + } + Ok(bindings) +} + +pub async fn write_partition_binding_to_catalog( + relation: &Relation, + binding: &TablePartitionBinding, + xid: u64, +) -> RS<()> { + let key = encode_partition_binding_catalog_key(binding.table_id)?; + let value = encode_partition_binding_catalog_value(binding)?; + relation.write_value(key, value, xid).await +} + +fn visible_snapshot_xid() -> u64 { + let base = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + .min((u64::MAX - 2) as u128) as u64; + base.saturating_add(1) +} diff --git a/mudu_kernel/src/meta/partition_placement_catalog.rs b/mudu_kernel/src/meta/partition_placement_catalog.rs new file mode 100644 index 0000000..14d9c51 --- /dev/null +++ b/mudu_kernel/src/meta/partition_placement_catalog.rs @@ -0,0 +1,133 @@ +use std::ops::Bound; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use mudu::common::endian; +use mudu::common::id::OID; +use mudu::common::result::RS; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dt_info::DTInfo; + +use crate::contract::partition_rule_binding::PartitionPlacement; +use crate::contract::schema_column::SchemaColumn; +use crate::contract::schema_table::SchemaTable; +use crate::contract::table_desc::TableDesc; +use crate::contract::table_info::TableInfo; +use crate::server::worker_snapshot::WorkerSnapshot; +use crate::storage::relation::relation::Relation; + +pub const PARTITION_PLACEMENT_CATALOG_PARTITION_ID: OID = 0; +pub const PARTITION_PLACEMENT_CATALOG_TABLE_ID: OID = 0x4; +const PARTITION_PLACEMENT_CATALOG_TABLE_NAME: &str = "__meta_partition_placement"; +const PARTITION_PLACEMENT_CATALOG_PARTITION_OID_COLUMN_ID: OID = 0x40001; +const PARTITION_PLACEMENT_CATALOG_PLACEMENT_COLUMN_ID: OID = 0x40002; + +pub fn partition_placement_catalog_schema() -> SchemaTable { + SchemaTable::new_with_oid( + PARTITION_PLACEMENT_CATALOG_TABLE_ID, + PARTITION_PLACEMENT_CATALOG_TABLE_NAME.to_string(), + vec![ + SchemaColumn::new_with_oid( + PARTITION_PLACEMENT_CATALOG_PARTITION_OID_COLUMN_ID, + "partition_oid".to_string(), + DatTypeID::U128, + DTInfo::from_text(DatTypeID::U128, String::new()), + ), + SchemaColumn::new_with_oid( + PARTITION_PLACEMENT_CATALOG_PLACEMENT_COLUMN_ID, + "placement".to_string(), + DatTypeID::Binary, + DTInfo::from_text(DatTypeID::Binary, String::new()), + ), + ], + vec![0], + vec![1], + ) +} + +pub fn partition_placement_catalog_desc() -> RS> { + TableInfo::new(partition_placement_catalog_schema())?.table_desc() +} + +pub fn open_partition_placement_catalog(path: &str) -> RS { + let desc = partition_placement_catalog_desc()?; + Ok(Relation::new( + PARTITION_PLACEMENT_CATALOG_TABLE_ID, + PARTITION_PLACEMENT_CATALOG_PARTITION_ID, + path.to_string(), + desc.as_ref(), + )) +} + +pub fn encode_partition_placement_catalog_key(oid: OID) -> RS> { + let mut key = vec![0; std::mem::size_of::()]; + endian::write_u128(&mut key, oid); + Ok(key) +} + +pub fn encode_partition_placement_catalog_value(placement: &PartitionPlacement) -> RS> { + rmp_serde::to_vec(placement).map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::EncodeErr, + "encode partition placement catalog value error", + e + ) + }) +} + +pub fn decode_partition_placement_catalog_key(tuple: &[u8]) -> RS { + Ok(endian::read_u128(tuple)) +} + +pub fn decode_partition_placement_catalog_value(tuple: &[u8]) -> RS { + rmp_serde::from_slice(tuple).map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::DecodeErr, + "decode partition placement catalog value error", + e + ) + }) +} + +pub fn load_partition_placements_from_catalog(relation: &Relation) -> RS> { + let rows = relation.visible_range_sync( + (Bound::Unbounded, Bound::Unbounded), + &WorkerSnapshot::new(visible_snapshot_xid(), vec![]), + )?; + let mut placements = Vec::with_capacity(rows.len()); + for (key, value) in rows { + let key_oid = decode_partition_placement_catalog_key(&key)?; + let placement = decode_partition_placement_catalog_value(&value)?; + if key_oid != placement.partition_id { + return Err(mudu::m_error!( + mudu::error::ec::EC::DecodeErr, + format!( + "partition placement catalog key oid {} does not match partition oid {}", + key_oid, + placement.partition_id + ) + )); + } + placements.push(placement); + } + Ok(placements) +} + +pub async fn write_partition_placement_to_catalog( + relation: &Relation, + placement: &PartitionPlacement, + xid: u64, +) -> RS<()> { + let key = encode_partition_placement_catalog_key(placement.partition_id)?; + let value = encode_partition_placement_catalog_value(placement)?; + relation.write_value(key, value, xid).await +} + +fn visible_snapshot_xid() -> u64 { + let base = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + .min((u64::MAX - 2) as u128) as u64; + base.saturating_add(1) +} diff --git a/mudu_kernel/src/meta/partition_rule_catalog.rs b/mudu_kernel/src/meta/partition_rule_catalog.rs new file mode 100644 index 0000000..20b5162 --- /dev/null +++ b/mudu_kernel/src/meta/partition_rule_catalog.rs @@ -0,0 +1,133 @@ +use std::ops::Bound; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use mudu::common::endian; +use mudu::common::id::OID; +use mudu::common::result::RS; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::dt_info::DTInfo; + +use crate::contract::partition_rule::PartitionRuleDesc; +use crate::contract::schema_column::SchemaColumn; +use crate::contract::schema_table::SchemaTable; +use crate::contract::table_desc::TableDesc; +use crate::contract::table_info::TableInfo; +use crate::server::worker_snapshot::WorkerSnapshot; +use crate::storage::relation::relation::Relation; + +pub const PARTITION_RULE_CATALOG_PARTITION_ID: OID = 0; +pub const PARTITION_RULE_CATALOG_TABLE_ID: OID = 0x2; +const PARTITION_RULE_CATALOG_TABLE_NAME: &str = "__meta_partition_rule"; +const PARTITION_RULE_CATALOG_RULE_OID_COLUMN_ID: OID = 0x20001; +const PARTITION_RULE_CATALOG_RULE_COLUMN_ID: OID = 0x20002; + +pub fn partition_rule_catalog_schema() -> SchemaTable { + SchemaTable::new_with_oid( + PARTITION_RULE_CATALOG_TABLE_ID, + PARTITION_RULE_CATALOG_TABLE_NAME.to_string(), + vec![ + SchemaColumn::new_with_oid( + PARTITION_RULE_CATALOG_RULE_OID_COLUMN_ID, + "rule_oid".to_string(), + DatTypeID::U128, + DTInfo::from_text(DatTypeID::U128, String::new()), + ), + SchemaColumn::new_with_oid( + PARTITION_RULE_CATALOG_RULE_COLUMN_ID, + "rule".to_string(), + DatTypeID::Binary, + DTInfo::from_text(DatTypeID::Binary, String::new()), + ), + ], + vec![0], + vec![1], + ) +} + +pub fn partition_rule_catalog_desc() -> RS> { + TableInfo::new(partition_rule_catalog_schema())?.table_desc() +} + +pub fn open_partition_rule_catalog(path: &str) -> RS { + let desc = partition_rule_catalog_desc()?; + Ok(Relation::new( + PARTITION_RULE_CATALOG_TABLE_ID, + PARTITION_RULE_CATALOG_PARTITION_ID, + path.to_string(), + desc.as_ref(), + )) +} + +pub fn encode_partition_rule_catalog_key(oid: OID) -> RS> { + let mut key = vec![0; std::mem::size_of::()]; + endian::write_u128(&mut key, oid); + Ok(key) +} + +pub fn encode_partition_rule_catalog_value(rule: &PartitionRuleDesc) -> RS> { + rmp_serde::to_vec(rule).map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::EncodeErr, + "encode partition rule catalog value error", + e + ) + }) +} + +pub fn decode_partition_rule_catalog_key(tuple: &[u8]) -> RS { + Ok(endian::read_u128(tuple)) +} + +pub fn decode_partition_rule_catalog_value(tuple: &[u8]) -> RS { + rmp_serde::from_slice(tuple).map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::DecodeErr, + "decode partition rule catalog value error", + e + ) + }) +} + +pub fn load_partition_rules_from_catalog(relation: &Relation) -> RS> { + let rows = relation.visible_range_sync( + (Bound::Unbounded, Bound::Unbounded), + &WorkerSnapshot::new(visible_snapshot_xid(), vec![]), + )?; + let mut rules = Vec::with_capacity(rows.len()); + for (key, value) in rows { + let key_oid = decode_partition_rule_catalog_key(&key)?; + let rule = decode_partition_rule_catalog_value(&value)?; + if key_oid != rule.oid { + return Err(mudu::m_error!( + mudu::error::ec::EC::DecodeErr, + format!( + "partition rule catalog key oid {} does not match rule oid {}", + key_oid, + rule.oid + ) + )); + } + rules.push(rule); + } + Ok(rules) +} + +pub async fn write_partition_rule_to_catalog( + relation: &Relation, + rule: &PartitionRuleDesc, + xid: u64, +) -> RS<()> { + let key = encode_partition_rule_catalog_key(rule.oid)?; + let value = encode_partition_rule_catalog_value(rule)?; + relation.write_value(key, value, xid).await +} + +fn visible_snapshot_xid() -> u64 { + let base = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + .min((u64::MAX - 2) as u128) as u64; + base.saturating_add(1) +} diff --git a/mudu_kernel/src/mudu_conn/mod.rs b/mudu_kernel/src/mudu_conn/mod.rs index 609c32e..604e8d4 100644 --- a/mudu_kernel/src/mudu_conn/mod.rs +++ b/mudu_kernel/src/mudu_conn/mod.rs @@ -1,4 +1,4 @@ -pub mod mudu_conn_core; pub mod mudu_conn_async; +pub mod mudu_conn_core; pub mod mudu_prepared_stmt; pub mod mudu_result_set_async; diff --git a/mudu_kernel/src/mudu_conn/mudu_conn_async.rs b/mudu_kernel/src/mudu_conn/mudu_conn_async.rs index ffa2c6f..aa6b474 100644 --- a/mudu_kernel/src/mudu_conn/mudu_conn_async.rs +++ b/mudu_kernel/src/mudu_conn/mudu_conn_async.rs @@ -9,27 +9,97 @@ use mudu_contract::database::prepared_stmt::PreparedStmt; use mudu_contract::database::result_set::ResultSetAsync; use mudu_contract::database::sql_params::SQLParams; use mudu_contract::database::sql_stmt::SQLStmt; +use mudu_contract::protocol::{ + ClientRequest, Frame, HEADER_LEN, MessageType, SessionCreateRequest, decode_error_response, + decode_server_response, decode_session_create_response, encode_batch_request, + encode_client_request_with_message_type, encode_session_create_request, +}; use sql_parser::ast::parser::SQLParser; use sql_parser::ast::stmt_type::StmtType; -use std::sync::Arc; -use tokio::sync::Mutex; +use std::sync::{Arc, Mutex, OnceLock}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::sync::Mutex as AsyncMutex; use crate::mudu_conn::mudu_prepared_stmt::MuduPreparedStmt; -use crate::server::worker_local::{current_worker_local, WorkerExecute, WorkerLocalRef}; +use crate::server::worker_local::{WorkerExecute, WorkerLocalRef, try_current_worker_local}; use crate::sql::describer::Describer; +static DEFAULT_REMOTE_ADDR: OnceLock>> = OnceLock::new(); +static DEFAULT_REMOTE_WORKER_ID: OnceLock>> = OnceLock::new(); + +enum ConnBackend { + WorkerLocal(WorkerLocalRef), + Remote(Arc), +} + +struct RemoteWorkerConn { + addr: String, + worker_id: Option, + session_id: AsyncMutex>, + stream: AsyncMutex>, +} + +struct RemoteProtocolClient { + stream: TcpStream, + next_request_id: u64, +} + pub struct MuduConnAsync { - worker_local: WorkerLocalRef, + backend: ConnBackend, parser: Arc, - session_id: Arc>>, + session_id: Arc>>, +} + +pub fn set_default_remote_addr(addr: Option) { + let slot = DEFAULT_REMOTE_ADDR.get_or_init(|| Mutex::new(None)); + if let Ok(mut guard) = slot.lock() { + *guard = addr; + } +} + +pub fn set_default_remote_worker_id(worker_id: Option) { + let slot = DEFAULT_REMOTE_WORKER_ID.get_or_init(|| Mutex::new(None)); + if let Ok(mut guard) = slot.lock() { + *guard = worker_id; + } +} + +fn default_remote_addr() -> Option { + DEFAULT_REMOTE_ADDR + .get() + .and_then(|slot| slot.lock().ok().and_then(|guard| guard.clone())) +} + +fn default_remote_worker_id() -> Option { + DEFAULT_REMOTE_WORKER_ID + .get() + .and_then(|slot| slot.lock().ok().and_then(|guard| *guard)) } impl MuduConnAsync { pub fn new() -> Self { + if let Some(worker_local) = try_current_worker_local() { + return Self { + backend: ConnBackend::WorkerLocal(worker_local), + parser: Arc::new(SQLParser::new()), + session_id: Arc::new(AsyncMutex::new(None)), + }; + } + let addr = default_remote_addr().unwrap_or_else(|| { + panic!("current worker local is not set and no default remote mududb addr is configured") + }); + let parser = Arc::new(SQLParser::new()); + let remote = Arc::new(RemoteWorkerConn { + addr, + worker_id: default_remote_worker_id(), + session_id: AsyncMutex::new(None), + stream: AsyncMutex::new(None), + }); Self { - worker_local: current_worker_local(), - parser: Arc::new(SQLParser::new()), - session_id: Arc::new(Mutex::new(None)), + backend: ConnBackend::Remote(remote), + parser, + session_id: Arc::new(AsyncMutex::new(None)), } } @@ -42,12 +112,66 @@ impl MuduConnAsync { Ok(stmts.remove(0)) } + async fn ensure_session_id(&self) -> RS { + match &self.backend { + ConnBackend::WorkerLocal(worker_local) => { + let mut guard = self.session_id.lock().await; + if let Some(session_id) = *guard { + return Ok(session_id); + } + let session_id = worker_local.open_async().await?; + *guard = Some(session_id); + Ok(session_id) + } + ConnBackend::Remote(remote) => remote.ensure_session_id().await, + } + } + + async fn active_session_id(&self) -> RS { + match &self.backend { + ConnBackend::WorkerLocal(_) => { + let guard = self.session_id.lock().await; + guard.ok_or_else(|| m_error!(EC::NoSuchElement, "no active session")) + } + ConnBackend::Remote(remote) => remote.active_session_id().await, + } + } +} + +impl RemoteWorkerConn { + async fn client(&self) -> RS>> { + let mut guard = self.stream.lock().await; + if guard.is_none() { + *guard = Some(RemoteProtocolClient::connect(&self.addr).await?); + } + Ok(guard) + } + async fn ensure_session_id(&self) -> RS { let mut guard = self.session_id.lock().await; if let Some(session_id) = *guard { return Ok(session_id); } - let session_id = self.worker_local.open_async().await?; + let mut client_guard = self.client().await?; + let client = client_guard + .as_mut() + .ok_or_else(|| m_error!(EC::InternalErr, "remote worker client is missing"))?; + let request_id = client.take_request_id(); + let config_json = self + .worker_id + .map(|worker_id| { + serde_json::json!({ + "session_id": 0, + "worker_id": worker_id.to_string() + }) + .to_string() + }); + let payload = encode_session_create_request( + request_id, + &SessionCreateRequest::new(config_json), + )?; + let frame = client.send_and_receive(&payload).await?; + let session_id = decode_session_create_response(&frame)?.session_id(); *guard = Some(session_id); Ok(session_id) } @@ -56,52 +180,178 @@ impl MuduConnAsync { let guard = self.session_id.lock().await; guard.ok_or_else(|| m_error!(EC::NoSuchElement, "no active session")) } + + async fn batch_sql(&self, sql: String) -> RS { + let _session_id = self.ensure_session_id().await?; + let mut client_guard = self.client().await?; + let client = client_guard + .as_mut() + .ok_or_else(|| m_error!(EC::InternalErr, "remote worker client is missing"))?; + let payload = encode_batch_request( + client.take_request_id(), + &ClientRequest::new("default", sql), + )?; + let frame = client.send_and_receive(&payload).await?; + Ok(decode_server_response(&frame)?.affected_rows()) + } + + async fn execute_sql(&self, sql: String) -> RS { + let _session_id = self.ensure_session_id().await?; + let mut client_guard = self.client().await?; + let client = client_guard + .as_mut() + .ok_or_else(|| m_error!(EC::InternalErr, "remote worker client is missing"))?; + let payload = encode_client_request_with_message_type( + MessageType::Execute, + client.take_request_id(), + &ClientRequest::new("default", sql), + )?; + let frame = client.send_and_receive(&payload).await?; + Ok(decode_server_response(&frame)?.affected_rows()) + } +} + +impl RemoteProtocolClient { + async fn connect(addr: &str) -> RS { + let stream = TcpStream::connect(addr).await.map_err(|e| { + m_error!( + EC::NetErr, + format!("connect io_uring tcp server error: addr={addr}"), + e + ) + })?; + stream + .set_nodelay(true) + .map_err(|e| m_error!(EC::NetErr, format!("set tcp nodelay error: addr={addr}"), e))?; + Ok(Self { + stream, + next_request_id: 1, + }) + } + + fn take_request_id(&mut self) -> u64 { + let request_id = self.next_request_id; + self.next_request_id += 1; + request_id + } + + async fn send_and_receive(&mut self, payload: &[u8]) -> RS { + self.stream + .write_all(payload) + .await + .map_err(|e| m_error!(EC::NetErr, "write request frame error", e))?; + self.stream + .flush() + .await + .map_err(|e| m_error!(EC::NetErr, "flush request frame error", e))?; + + let mut header = [0u8; HEADER_LEN]; + self.stream + .read_exact(&mut header) + .await + .map_err(|e| m_error!(EC::NetErr, "read response header error", e))?; + let payload_len = + u32::from_be_bytes([header[16], header[17], header[18], header[19]]) as usize; + let mut frame_bytes = Vec::with_capacity(HEADER_LEN + payload_len); + frame_bytes.extend_from_slice(&header); + if payload_len > 0 { + let mut body = vec![0u8; payload_len]; + self.stream + .read_exact(&mut body) + .await + .map_err(|e| m_error!(EC::NetErr, "read response payload error", e))?; + frame_bytes.extend_from_slice(&body); + } + let frame = Frame::decode(&frame_bytes)?; + if frame.header().message_type() == MessageType::Error { + let error = decode_error_response(&frame)?; + return Err(m_error!(EC::NetErr, error.message())); + } + Ok(frame) + } } #[async_trait] impl DBConnAsync for MuduConnAsync { async fn prepare(&self, stmt: Box) -> RS> { - let parsed = self.parse_one(stmt.as_ref())?; - let desc = Describer:: - describe(self.worker_local.meta_mgr().as_ref(), parsed) - .await?; - Ok(Arc::new(MuduPreparedStmt::new( - self.worker_local.clone(), - self.session_id.clone(), - stmt, - Arc::new(desc), - ))) + match &self.backend { + ConnBackend::WorkerLocal(worker_local) => { + let parsed = self.parse_one(stmt.as_ref())?; + let desc = Describer::describe(worker_local.meta_mgr().as_ref(), parsed).await?; + Ok(Arc::new(MuduPreparedStmt::new( + worker_local.clone(), + self.session_id.clone(), + stmt, + Arc::new(desc), + ))) + } + ConnBackend::Remote(_) => Err(m_error!( + EC::NotImplemented, + "prepare is not supported without worker-local context" + )), + } } async fn exec_silent(&self, sql_text: String) -> RS<()> { - let session_id = self.ensure_session_id().await?; - let _ = self - .worker_local - .batch(session_id, Box::new(sql_text), Box::new(())) - .await?; - Ok(()) + match &self.backend { + ConnBackend::WorkerLocal(worker_local) => { + let session_id = self.ensure_session_id().await?; + let _ = worker_local + .batch(session_id, Box::new(sql_text), Box::new(())) + .await?; + Ok(()) + } + ConnBackend::Remote(remote) => { + let _ = remote.batch_sql(sql_text).await?; + Ok(()) + } + } } async fn begin_tx(&self) -> RS { let session_id = self.ensure_session_id().await?; - self.worker_local - .execute_async(session_id, WorkerExecute::BeginTx) - .await?; - Ok(session_id) + match &self.backend { + ConnBackend::WorkerLocal(worker_local) => { + worker_local + .execute_async(session_id, WorkerExecute::BeginTx) + .await?; + Ok(session_id) + } + ConnBackend::Remote(_) => Err(m_error!( + EC::NotImplemented, + "transaction control is not supported without worker-local context" + )), + } } async fn rollback_tx(&self) -> RS<()> { let session_id = self.active_session_id().await?; - self.worker_local - .execute_async(session_id, WorkerExecute::RollbackTx) - .await + match &self.backend { + ConnBackend::WorkerLocal(worker_local) => { + worker_local + .execute_async(session_id, WorkerExecute::RollbackTx) + .await + } + ConnBackend::Remote(_) => Err(m_error!( + EC::NotImplemented, + "transaction control is not supported without worker-local context" + )), + } } async fn commit_tx(&self) -> RS<()> { let session_id = self.active_session_id().await?; - self.worker_local - .execute_async(session_id, WorkerExecute::CommitTx) - .await + match &self.backend { + ConnBackend::WorkerLocal(worker_local) => { + worker_local + .execute_async(session_id, WorkerExecute::CommitTx) + .await + } + ConnBackend::Remote(_) => Err(m_error!( + EC::NotImplemented, + "transaction control is not supported without worker-local context" + )), + } } async fn query( @@ -109,17 +359,51 @@ impl DBConnAsync for MuduConnAsync { sql: Box, param: Box, ) -> RS> { - let session_id = self.ensure_session_id().await?; - self.worker_local.query(session_id, sql, param).await + match &self.backend { + ConnBackend::WorkerLocal(worker_local) => { + let session_id = self.ensure_session_id().await?; + worker_local.query(session_id, sql, param).await + } + ConnBackend::Remote(_) => Err(m_error!( + EC::NotImplemented, + "query is not supported without worker-local context" + )), + } } async fn execute(&self, sql: Box, param: Box) -> RS { - let session_id = self.ensure_session_id().await?; - self.worker_local.execute(session_id, sql, param).await + match &self.backend { + ConnBackend::WorkerLocal(worker_local) => { + let session_id = self.ensure_session_id().await?; + worker_local.execute(session_id, sql, param).await + } + ConnBackend::Remote(remote) => { + if param.size() != 0 { + return Err(m_error!( + EC::NotImplemented, + "execute with parameters is not supported without worker-local context" + )); + } + remote.execute_sql(sql.to_sql_string()).await + } + } } async fn batch(&self, sql: Box, param: Box) -> RS { - let session_id = self.ensure_session_id().await?; - self.worker_local.batch(session_id, sql, param).await + match &self.backend { + ConnBackend::WorkerLocal(worker_local) => { + let session_id = self.ensure_session_id().await?; + worker_local.batch(session_id, sql, param).await + } + ConnBackend::Remote(remote) => { + if param.size() != 0 { + return Err(m_error!( + EC::NotImplemented, + "batch with parameters is not supported without worker-local context" + )); + } + remote.batch_sql(sql.to_sql_string()).await + } + } } } diff --git a/mudu_kernel/src/server/connection_state.rs b/mudu_kernel/src/server/connection_state.rs new file mode 100644 index 0000000..ff125a8 --- /dev/null +++ b/mudu_kernel/src/server/connection_state.rs @@ -0,0 +1,12 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConnectionState { + Accepted, + RoutingPending, + Active, + WaitingComponent, + WaitingStorage, + Sending, + Closing, + Closed, +} + diff --git a/mudu_kernel/src/server/connection_worker_task.rs b/mudu_kernel/src/server/connection_worker_task.rs index f047ce2..c1eb74e 100644 --- a/mudu_kernel/src/server/connection_worker_task.rs +++ b/mudu_kernel/src/server/connection_worker_task.rs @@ -134,7 +134,7 @@ fn build_transfer( ConnectionTransfer::new( conn_id, transfer.target_worker(), - crate::server::fsm::ConnectionState::Active, + crate::server::connection_state::ConnectionState::Active, remote_addr, ), socket.into_raw_fd(), diff --git a/mudu_kernel/src/server/fsm.rs b/mudu_kernel/src/server/fsm.rs deleted file mode 100644 index e107430..0000000 --- a/mudu_kernel/src/server/fsm.rs +++ /dev/null @@ -1,46 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConnectionState { - Accepted, - RoutingPending, - Active, - WaitingComponent, - WaitingStorage, - Sending, - Closing, - Closed, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WorkerEvent { - Accepted, - RoutedLocal, - RoutedRemote, - RequestDecoded, - StoragePending, - StorageComplete, - ComponentPending, - ComponentReady, - ResponseQueued, - PeerClosed, - FatalError, -} - -pub fn advance_state(state: ConnectionState, event: WorkerEvent) -> ConnectionState { - match (state, event) { - (ConnectionState::Accepted, WorkerEvent::Accepted) => ConnectionState::RoutingPending, - (ConnectionState::RoutingPending, WorkerEvent::RoutedLocal) => ConnectionState::Active, - (ConnectionState::RoutingPending, WorkerEvent::RoutedRemote) => ConnectionState::Closed, - (ConnectionState::Active, WorkerEvent::RequestDecoded) => ConnectionState::WaitingStorage, - (ConnectionState::WaitingStorage, WorkerEvent::StorageComplete) => ConnectionState::Sending, - (ConnectionState::Active, WorkerEvent::ComponentPending) => { - ConnectionState::WaitingComponent - } - (ConnectionState::WaitingComponent, WorkerEvent::ComponentReady) => { - ConnectionState::Sending - } - (ConnectionState::Sending, WorkerEvent::ResponseQueued) => ConnectionState::Active, - (_, WorkerEvent::PeerClosed) | (_, WorkerEvent::FatalError) => ConnectionState::Closing, - (ConnectionState::Closing, _) => ConnectionState::Closed, - _ => state, - } -} diff --git a/mudu_kernel/src/server/message_bus_api.rs b/mudu_kernel/src/server/message_bus_api.rs new file mode 100644 index 0000000..87c8d14 --- /dev/null +++ b/mudu_kernel/src/server/message_bus_api.rs @@ -0,0 +1,283 @@ +use async_trait::async_trait; +use mudu::common::id::OID; +use mudu::common::result::RS; +use mudu::error::ec::EC; +use mudu::m_error; +use std::cell::UnsafeCell; +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex, OnceLock}; + +pub type MessageId = u64; +pub type SubscriptionId = u64; +pub type MessageCallbackFuture = Pin> + 'static>>; +pub type OnRecvCallback = Arc MessageCallbackFuture + 'static>; + +thread_local! { + static CURRENT_MESSAGE_BUS: UnsafeCell> = + const { UnsafeCell::new(None) }; +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum EndpointId { + Worker(OID), + External(u128), + Session(OID), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum DeliveryMode { + FireAndForget, + Request, + Response, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum SystemMessageKind { + Ack, + Nack, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MessageKind { + User(u16), + System(SystemMessageKind), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Envelope { + msg_id: MessageId, + correlation_id: Option, + src: EndpointId, + dst: EndpointId, + kind: MessageKind, + payload: Vec, + delivery: DeliveryMode, +} + +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct RecvFilter { + pub src: Option, + pub dst: Option, + pub kind: Option, + pub correlation_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OutgoingMessage { + kind: MessageKind, + payload: Vec, + correlation_id: Option, + delivery: DeliveryMode, +} + +#[async_trait(?Send)] +pub trait MessageBus { + fn local_endpoint(&self) -> EndpointId; + + async fn send(&self, dst: EndpointId, message: OutgoingMessage) -> RS; + + async fn recv(&self, filter: RecvFilter) -> RS; + + fn on_recv_callback(&self, filter: RecvFilter, callback: OnRecvCallback) -> RS; + + fn cancel_callback(&self, id: SubscriptionId) -> RS; +} + +pub type MessageBusRef = Arc; + +#[derive(Clone, Copy)] +struct MessageBusPtr(*const dyn MessageBus); + +// Safety: the registry stores raw Arc pointers behind a mutex, holds one strong reference for +// each entry, and drops that reference on unregister. +unsafe impl Send for MessageBusPtr {} +unsafe impl Sync for MessageBusPtr {} + +fn message_bus_registry() -> &'static Mutex> { + static REGISTRY: OnceLock>> = OnceLock::new(); + REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) +} + +impl Envelope { + pub fn new( + msg_id: MessageId, + correlation_id: Option, + src: EndpointId, + dst: EndpointId, + kind: MessageKind, + payload: Vec, + delivery: DeliveryMode, + ) -> Self { + Self { + msg_id, + correlation_id, + src, + dst, + kind, + payload, + delivery, + } + } + + pub fn msg_id(&self) -> MessageId { + self.msg_id + } + + pub fn correlation_id(&self) -> Option { + self.correlation_id + } + + pub fn src(&self) -> &EndpointId { + &self.src + } + + pub fn dst(&self) -> &EndpointId { + &self.dst + } + + pub fn kind(&self) -> MessageKind { + self.kind + } + + pub fn payload(&self) -> &[u8] { + &self.payload + } + + pub fn payload_owned(&self) -> Vec { + self.payload.clone() + } + + pub fn delivery(&self) -> DeliveryMode { + self.delivery + } + + pub fn matches(&self, filter: &RecvFilter) -> bool { + filter.src.as_ref().is_none_or(|src| src == self.src()) + && filter.dst.as_ref().is_none_or(|dst| dst == self.dst()) + && filter.kind.is_none_or(|kind| kind == self.kind()) + && filter + .correlation_id + .is_none_or(|correlation_id| Some(correlation_id) == self.correlation_id()) + } +} + +impl OutgoingMessage { + pub fn new(kind: MessageKind, payload: Vec) -> Self { + Self { + kind, + payload, + correlation_id: None, + delivery: DeliveryMode::FireAndForget, + } + } + + pub fn with_correlation_id(mut self, correlation_id: MessageId) -> Self { + self.correlation_id = Some(correlation_id); + self + } + + pub fn with_delivery(mut self, delivery: DeliveryMode) -> Self { + self.delivery = delivery; + self + } + + pub fn kind(&self) -> MessageKind { + self.kind + } + + pub fn payload(&self) -> &[u8] { + &self.payload + } + + pub fn payload_owned(&self) -> Vec { + self.payload.clone() + } + + pub fn correlation_id(&self) -> Option { + self.correlation_id + } + + pub fn delivery(&self) -> DeliveryMode { + self.delivery + } +} + +pub(crate) fn set_current_message_bus(message_bus: MessageBusRef) { + CURRENT_MESSAGE_BUS.with(|slot| { + // Safety: the slot is thread-local and only mutated through these helpers. + unsafe { + *slot.get() = Some(message_bus); + } + }); +} + +pub(crate) fn unset_current_message_bus() { + CURRENT_MESSAGE_BUS.with(|slot| { + // Safety: the slot is thread-local and only mutated through these helpers. + unsafe { + *slot.get() = None; + } + }); +} + +#[allow(dead_code)] +pub(crate) fn current_message_bus() -> RS { + CURRENT_MESSAGE_BUS.with(|slot| { + // Safety: shared reads are confined to the current thread-local slot. + let message_bus = unsafe { &*slot.get() }; + message_bus + .as_ref() + .cloned() + .ok_or_else(|| m_error!(EC::NoSuchElement, "current message bus is not set")) + }) +} + +pub(crate) fn register_worker_message_bus(worker_id: OID, message_bus: &MessageBusRef) -> RS<()> { + let raw = Arc::into_raw(message_bus.clone()); + let mut registry = message_bus_registry() + .lock() + .map_err(|_| m_error!(EC::InternalErr, "message bus registry lock poisoned"))?; + if let Some(old) = registry.insert(worker_id, MessageBusPtr(raw)) { + // Safety: the registry owns one strong ref per registered pointer. + unsafe { + drop(Arc::from_raw(old.0)); + } + } + Ok(()) +} + +pub(crate) fn unregister_worker_message_bus(worker_id: OID) -> RS<()> { + let mut registry = message_bus_registry() + .lock() + .map_err(|_| m_error!(EC::InternalErr, "message bus registry lock poisoned"))?; + let Some(raw) = registry.remove(&worker_id) else { + return Ok(()); + }; + // Safety: the registry owns one strong ref per registered pointer. + unsafe { + drop(Arc::from_raw(raw.0)); + } + Ok(()) +} + +pub(crate) fn message_bus_for_worker(worker_id: OID) -> RS { + let raw = { + let registry = message_bus_registry() + .lock() + .map_err(|_| m_error!(EC::InternalErr, "message bus registry lock poisoned"))?; + registry.get(&worker_id).copied().ok_or_else(|| { + m_error!( + EC::NoSuchElement, + format!("message bus for worker {} is not registered", worker_id) + ) + })? + }; + // Safety: the registry entry came from `Arc::into_raw`; we temporarily bump the strong count + // to materialize a cloned Arc for the caller. + unsafe { + Arc::increment_strong_count(raw.0); + Ok(Arc::from_raw(raw.0)) + } +} diff --git a/mudu_kernel/src/server/message_bus_runtime.rs b/mudu_kernel/src/server/message_bus_runtime.rs new file mode 100644 index 0000000..806b7a7 --- /dev/null +++ b/mudu_kernel/src/server/message_bus_runtime.rs @@ -0,0 +1,355 @@ +use async_trait::async_trait; +use crossbeam_queue::SegQueue; +use mudu::common::id::OID; +use mudu::common::result::RS; +use mudu::error::ec::EC; +use mudu::m_error; +use std::collections::VecDeque; +use std::os::fd::RawFd; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use tokio::sync::oneshot; + +use crate::io::worker_ring::WorkerLocalRing; +use crate::server::message_bus_api::{ + EndpointId, Envelope, MessageBus, MessageBusRef, MessageId, OnRecvCallback, + OutgoingMessage, RecvFilter, SubscriptionId, +}; +use crate::server::server_iouring; +use crate::server::worker_mailbox::WorkerMailboxMsg; +use crate::server::worker_registry::WorkerRegistry; +use crate::server::worker_task::spawn_system_worker_task; + +struct RecvWaiter { + filter: RecvFilter, + sender: oneshot::Sender, +} + +struct RegisteredCallback { + id: SubscriptionId, + filter: RecvFilter, + callback: OnRecvCallback, +} + +#[derive(Default)] +struct MessageBusState { + inbox: VecDeque, + recv_waiters: VecDeque, + callbacks: Vec, + next_subscription_id: SubscriptionId, +} + +pub(crate) struct WorkerMessageBus { + local_worker_id: OID, + registry: Arc, + mailbox_fds: Vec, + mailboxes: Vec>>, + worker_local_ring: Arc, + next_msg_id: AtomicU64, + state: Mutex, +} + +impl WorkerMessageBus { + pub(crate) fn new( + local_worker_id: OID, + registry: Arc, + mailbox_fds: Vec, + mailboxes: Vec>>, + worker_local_ring: Arc, + ) -> Arc { + Arc::new(Self { + local_worker_id, + registry, + mailbox_fds, + mailboxes, + worker_local_ring, + next_msg_id: AtomicU64::new(1), + state: Mutex::new(MessageBusState { + next_subscription_id: 1, + ..MessageBusState::default() + }), + }) + } + + pub(crate) fn as_ref(self: &Arc) -> MessageBusRef { + self.clone() + } + + pub(crate) fn handle_incoming(&self, envelope: Envelope) -> RS<()> { + let maybe_callback = { + let mut state = self + .state + .lock() + .map_err(|_| m_error!(EC::InternalErr, "message bus state lock poisoned"))?; + state.handle_incoming(envelope) + }; + if let Some((callback, envelope)) = maybe_callback { + let future = (callback)(envelope); + self.worker_local_ring + .worker_task_registry() + .spawn_system(spawn_system_worker_task(future)); + } + Ok(()) + } + + fn route_worker_index(&self, endpoint: &EndpointId) -> RS { + match endpoint { + EndpointId::Worker(worker_id) => self + .registry + .worker_index_by_worker_id(*worker_id) + .ok_or_else(|| { + m_error!( + EC::NoSuchElement, + format!("no such worker id {}", worker_id) + ) + }), + EndpointId::External(external_id) => Err(m_error!( + EC::NotImplemented, + format!("external endpoint {} is not implemented yet", external_id) + )), + EndpointId::Session(session_id) => Err(m_error!( + EC::NotImplemented, + format!("session endpoint {} is not implemented yet", session_id) + )), + } + } + + fn dispatch_mailbox_message(&self, target_worker: usize, msg: WorkerMailboxMsg) -> RS<()> { + let Some(mailbox) = self.mailboxes.get(target_worker) else { + return Err(m_error!( + EC::InternalErr, + format!("mailbox target worker {} is out of range", target_worker) + )); + }; + let Some(&fd) = self.mailbox_fds.get(target_worker) else { + return Err(m_error!( + EC::InternalErr, + format!( + "mailbox eventfd target worker {} is out of range", + target_worker + ) + )); + }; + mailbox.push(msg); + server_iouring::notify_mailbox_fd(fd) + } +} + +#[async_trait(?Send)] +impl MessageBus for WorkerMessageBus { + fn local_endpoint(&self) -> EndpointId { + EndpointId::Worker(self.local_worker_id) + } + + async fn send(&self, dst: EndpointId, message: OutgoingMessage) -> RS { + let msg_id = self.next_msg_id.fetch_add(1, Ordering::Relaxed); + let envelope = Envelope::new( + msg_id, + message.correlation_id(), + self.local_endpoint(), + dst.clone(), + message.kind(), + message.payload_owned(), + message.delivery(), + ); + let target_worker = self.route_worker_index(&dst)?; + self.dispatch_mailbox_message(target_worker, WorkerMailboxMsg::BusMessage(envelope))?; + Ok(msg_id) + } + + async fn recv(&self, filter: RecvFilter) -> RS { + let receiver = { + let mut state = self + .state + .lock() + .map_err(|_| m_error!(EC::InternalErr, "message bus state lock poisoned"))?; + if let Some(envelope) = state.try_take_message(&filter) { + return Ok(envelope); + } + state.register_waiter(filter) + }; + receiver + .await + .map_err(|_| m_error!(EC::ThreadErr, "message bus waiter dropped before delivery")) + } + + fn on_recv_callback(&self, filter: RecvFilter, callback: OnRecvCallback) -> RS { + let (callback_id, maybe_envelope) = { + let mut state = self + .state + .lock() + .map_err(|_| m_error!(EC::InternalErr, "message bus state lock poisoned"))?; + state.register_callback(filter, callback.clone()) + }; + if let Some(envelope) = maybe_envelope { + let future = (callback)(envelope); + self.worker_local_ring + .worker_task_registry() + .spawn_system(spawn_system_worker_task(future)); + } + Ok(callback_id) + } + + fn cancel_callback(&self, id: SubscriptionId) -> RS { + let mut state = self + .state + .lock() + .map_err(|_| m_error!(EC::InternalErr, "message bus state lock poisoned"))?; + Ok(state.cancel_callback(id)) + } +} + +impl MessageBusState { + fn try_take_message(&mut self, filter: &RecvFilter) -> Option { + let index = self + .inbox + .iter() + .position(|message| message.matches(filter))?; + self.inbox.remove(index) + } + + fn register_waiter(&mut self, filter: RecvFilter) -> oneshot::Receiver { + let (sender, receiver) = oneshot::channel(); + self.recv_waiters.push_back(RecvWaiter { filter, sender }); + receiver + } + + fn register_callback( + &mut self, + filter: RecvFilter, + callback: OnRecvCallback, + ) -> (SubscriptionId, Option) { + let id = self.next_subscription_id; + self.next_subscription_id += 1; + let maybe_envelope = self.try_take_message(&filter); + self.callbacks.push(RegisteredCallback { + id, + filter, + callback, + }); + (id, maybe_envelope) + } + + fn cancel_callback(&mut self, id: SubscriptionId) -> bool { + let Some(index) = self.callbacks.iter().position(|callback| callback.id == id) else { + return false; + }; + self.callbacks.remove(index); + true + } + + fn handle_incoming(&mut self, envelope: Envelope) -> Option<(OnRecvCallback, Envelope)> { + if let Some(index) = self + .recv_waiters + .iter() + .position(|waiter| envelope.matches(&waiter.filter)) + { + if let Some(waiter) = self.recv_waiters.remove(index) { + let _ = waiter.sender.send(envelope); + return None; + } + } + + if let Some(index) = self + .callbacks + .iter() + .position(|callback| envelope.matches(&callback.filter)) + { + let callback = self.callbacks[index].callback.clone(); + return Some((callback, envelope)); + } + + self.inbox.push_back(envelope); + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::io::worker_ring::WorkerLocalRing; + use crate::server::message_bus_api::{DeliveryMode, MessageKind, SystemMessageKind}; + use crate::server::worker_registry::WorkerRegistry; + + fn test_registry() -> Arc { + Arc::new( + WorkerRegistry::new(vec![ + crate::server::worker_registry::WorkerIdentity { + worker_index: 0, + worker_id: 11, + partition_ids: vec![101], + }, + crate::server::worker_registry::WorkerIdentity { + worker_index: 1, + worker_id: 12, + partition_ids: vec![102], + }, + ]) + .unwrap(), + ) + } + + fn test_bus(worker_id: OID) -> Arc { + WorkerMessageBus::new( + worker_id, + test_registry(), + vec![0, 1], + vec![Arc::new(SegQueue::new()), Arc::new(SegQueue::new())], + Arc::new(WorkerLocalRing::new()), + ) + } + + #[tokio::test] + async fn recv_consumes_buffered_message() { + let bus = test_bus(11); + bus.handle_incoming(Envelope::new( + 1, + None, + EndpointId::Worker(12), + EndpointId::Worker(11), + MessageKind::User(7), + b"ping".to_vec(), + DeliveryMode::FireAndForget, + )) + .unwrap(); + + let message = bus + .recv(RecvFilter { + src: Some(EndpointId::Worker(12)), + kind: Some(MessageKind::User(7)), + ..RecvFilter::default() + }) + .await + .unwrap(); + assert_eq!(message.payload(), b"ping"); + } + + #[tokio::test] + async fn recv_waiter_is_fulfilled_by_incoming_message() { + let bus = test_bus(11); + let mut recv = Box::pin(bus.recv(RecvFilter { + src: Some(EndpointId::Worker(12)), + correlation_id: Some(9), + ..RecvFilter::default() + })); + + assert!(matches!( + futures::poll!(recv.as_mut()), + std::task::Poll::Pending + )); + + bus.handle_incoming(Envelope::new( + 2, + Some(9), + EndpointId::Worker(12), + EndpointId::Worker(11), + MessageKind::System(SystemMessageKind::Ack), + Vec::new(), + DeliveryMode::Response, + )) + .unwrap(); + + let message = recv.await.unwrap(); + assert_eq!(message.correlation_id(), Some(9)); + } +} diff --git a/mudu_kernel/src/server/mod.rs b/mudu_kernel/src/server/mod.rs index 811fc3e..ae921ba 100644 --- a/mudu_kernel/src/server/mod.rs +++ b/mudu_kernel/src/server/mod.rs @@ -13,7 +13,7 @@ mod callback_registry; #[cfg(target_os = "linux")] mod connection_worker_task; mod frame_dispatch; -pub mod fsm; +pub mod connection_state; mod handlers; #[cfg(target_os = "linux")] mod inflight_op; @@ -21,7 +21,12 @@ mod inflight_op; mod loop_mailbox; #[cfg(target_os = "linux")] mod loop_user_io; +pub mod message_bus_api; +#[cfg(target_os = "linux")] +mod message_bus_runtime; mod message_dispatcher; +pub mod partition_router; +mod partition_rpc; #[cfg(all(test, target_os = "linux"))] mod perf_test; #[cfg(target_os = "linux")] diff --git a/mudu_kernel/src/server/partition_router.rs b/mudu_kernel/src/server/partition_router.rs new file mode 100644 index 0000000..58b74d4 --- /dev/null +++ b/mudu_kernel/src/server/partition_router.rs @@ -0,0 +1,379 @@ +use std::ops::Bound; +use std::sync::Arc; + +use mudu::common::buf::Buf; +use mudu::common::id::OID; +use mudu::common::result::RS; +use mudu::error::ec::EC; +use mudu::m_error; +use mudu_contract::tuple::build_tuple::build_tuple; +use mudu_contract::tuple::comparator::tuple_compare; +use mudu_contract::tuple::tuple_binary_desc::TupleBinaryDesc; +use mudu_type::dat_type_id::DatTypeID; +use mudu_type::datum::DatumDyn; + +use crate::contract::meta_mgr::MetaMgr; +use crate::contract::partition_rule::{PartitionBound, PartitionRuleDesc}; +use crate::contract::table_desc::TableDesc; +use crate::x_engine::api::VecDatum; + +pub const DEFAULT_UNPARTITIONED_TABLE_PARTITION_ID: OID = 0; + +pub struct PartitionRouter { + meta_mgr: Arc, +} + +impl PartitionRouter { + pub fn new(meta_mgr: Arc) -> Self { + Self { meta_mgr } + } + + pub async fn route_exact_partition( + &self, + table_id: OID, + table_desc: &TableDesc, + key: &VecDatum, + ) -> RS> { + let Some(binding) = self.meta_mgr.get_table_partition_binding(table_id).await? else { + return Ok(Some(DEFAULT_UNPARTITIONED_TABLE_PARTITION_ID)); + }; + let rule = self.meta_mgr.get_partition_rule_by_id(binding.rule_id).await?; + let route_tuple = build_route_tuple(table_desc, &binding.ref_attr_indices, key)?; + let route_desc = build_route_tuple_desc(table_desc, &binding.ref_attr_indices)?; + + for partition in &rule.partitions { + let after_start = match &partition.start { + PartitionBound::Unbounded => true, + PartitionBound::Value(values) => { + let bound = build_partition_bound_tuple(&route_desc, values)?; + !tuple_compare(&route_desc, &route_tuple, &bound)?.is_lt() + } + }; + let before_end = match &partition.end { + PartitionBound::Unbounded => true, + PartitionBound::Value(values) => { + let bound = build_partition_bound_tuple(&route_desc, values)?; + tuple_compare(&route_desc, &route_tuple, &bound)?.is_lt() + } + }; + if after_start && before_end { + return Ok(Some(partition.partition_id)); + } + } + + Err(m_error!( + EC::NoSuchElement, + format!("no partition matched table {} key", table_id) + )) + } + + pub async fn route_range_partitions( + &self, + table_id: OID, + table_desc: &TableDesc, + start: &Bound>, + end: &Bound>, + ) -> RS>> { + let Some(binding) = self.meta_mgr.get_table_partition_binding(table_id).await? else { + return Ok(Some(vec![DEFAULT_UNPARTITIONED_TABLE_PARTITION_ID])); + }; + let rule = self.meta_mgr.get_partition_rule_by_id(binding.rule_id).await?; + let route_desc = build_route_tuple_desc(table_desc, &binding.ref_attr_indices)?; + let start_tuple = build_route_bound_tuple(table_desc, &binding.ref_attr_indices, start)?; + let end_tuple = build_route_bound_tuple(table_desc, &binding.ref_attr_indices, end)?; + let mut partitions = Vec::new(); + for partition in &rule.partitions { + if partition_overlaps(&rule, &route_desc, partition.partition_id, &start_tuple, &end_tuple)? { + partitions.push(partition.partition_id); + } + } + Ok(Some(partitions)) + } + + pub fn route_rule_exact_partition( + &self, + rule: &PartitionRuleDesc, + key_values: &[Vec], + ) -> RS { + let route_desc = build_rule_tuple_desc(&rule.key_types)?; + let route_tuple = build_partition_bound_tuple(&route_desc, key_values)?; + for partition in &rule.partitions { + let after_start = match &partition.start { + PartitionBound::Unbounded => true, + PartitionBound::Value(values) => { + let bound = build_partition_bound_tuple(&route_desc, values)?; + !tuple_compare(&route_desc, &route_tuple, &bound)?.is_lt() + } + }; + let before_end = match &partition.end { + PartitionBound::Unbounded => true, + PartitionBound::Value(values) => { + let bound = build_partition_bound_tuple(&route_desc, values)?; + tuple_compare(&route_desc, &route_tuple, &bound)?.is_lt() + } + }; + if after_start && before_end { + return Ok(partition.partition_id); + } + } + Err(m_error!( + EC::NoSuchElement, + format!("no partition matched rule {}", rule.name) + )) + } + + pub fn route_rule_range_partitions( + &self, + rule: &PartitionRuleDesc, + start: &Bound>>, + end: &Bound>>, + ) -> RS> { + let route_desc = build_rule_tuple_desc(&rule.key_types)?; + let start_tuple = build_rule_bound_tuple(&route_desc, start)?; + let end_tuple = build_rule_bound_tuple(&route_desc, end)?; + let mut partitions = Vec::new(); + for partition in &rule.partitions { + if partition_overlaps(rule, &route_desc, partition.partition_id, &start_tuple, &end_tuple)? + { + partitions.push(partition.partition_id); + } + } + Ok(partitions) + } +} + +fn build_rule_tuple_desc(key_types: &[DatTypeID]) -> RS { + let types = key_types + .iter() + .map(|id| mudu_type::dat_type::DatType::default_for(*id)) + .collect(); + TupleBinaryDesc::from(types) +} + +fn build_rule_bound_tuple( + route_desc: &TupleBinaryDesc, + bound: &Bound>>, +) -> RS>> { + match bound { + Bound::Included(values) => Ok(Bound::Included(build_partition_bound_tuple(route_desc, values)?)), + Bound::Excluded(values) => Ok(Bound::Excluded(build_partition_bound_tuple(route_desc, values)?)), + Bound::Unbounded => Ok(Bound::Unbounded), + } +} + +fn partition_overlaps( + rule: &PartitionRuleDesc, + route_desc: &TupleBinaryDesc, + partition_id: OID, + start: &Bound>, + end: &Bound>, +) -> RS { + let partition = rule + .partitions + .iter() + .find(|partition| partition.partition_id == partition_id) + .ok_or_else(|| m_error!(EC::NoSuchElement, format!("no such partition {}", partition_id)))?; + + let start_ok = match (end, &partition.start) { + (Bound::Unbounded, _) | (_, PartitionBound::Unbounded) => true, + (Bound::Included(end), PartitionBound::Value(bound_start)) + | (Bound::Excluded(end), PartitionBound::Value(bound_start)) => { + let start_tuple = build_partition_bound_tuple(route_desc, bound_start)?; + !tuple_compare(route_desc, end, &start_tuple)?.is_le() + } + }; + let end_ok = match (start, &partition.end) { + (Bound::Unbounded, _) | (_, PartitionBound::Unbounded) => true, + (Bound::Included(start), PartitionBound::Value(bound_end)) + | (Bound::Excluded(start), PartitionBound::Value(bound_end)) => { + let end_tuple = build_partition_bound_tuple(route_desc, bound_end)?; + tuple_compare(route_desc, start, &end_tuple)?.is_lt() + } + }; + Ok(start_ok && end_ok) +} + +fn build_route_tuple_desc(table_desc: &TableDesc, ref_attrs: &[usize]) -> RS { + let mut fields = ref_attrs + .iter() + .map(|attr| { + let field = table_desc.get_attr(*attr); + (field.type_desc().clone(), field.datum_index() as usize) + }) + .collect::>(); + fields.sort_by_key(|(_, datum_index)| *datum_index); + let types = fields.into_iter().map(|(dat_type, _)| dat_type).collect(); + TupleBinaryDesc::from(types) +} + +fn build_route_tuple(table_desc: &TableDesc, ref_attrs: &[usize], key: &VecDatum) -> RS> { + let mut values = Vec::with_capacity(ref_attrs.len()); + for attr in ref_attrs { + let binary = key + .data() + .iter() + .find_map(|(current_attr, binary)| (*current_attr == *attr).then(|| binary.clone())) + .ok_or_else(|| { + m_error!( + EC::NoSuchElement, + format!("missing partition route attribute {}", attr) + ) + })?; + values.push(binary); + } + build_tuple(&values, &build_route_tuple_desc(table_desc, ref_attrs)?) +} + +fn build_route_bound_tuple( + table_desc: &TableDesc, + ref_attrs: &[usize], + bound: &Bound>, +) -> RS>> { + match bound { + Bound::Included(values) => Ok(Bound::Included(build_route_tuple( + table_desc, + ref_attrs, + &VecDatum::new(values.clone()), + )?)), + Bound::Excluded(values) => Ok(Bound::Excluded(build_route_tuple( + table_desc, + ref_attrs, + &VecDatum::new(values.clone()), + )?)), + Bound::Unbounded => Ok(Bound::Unbounded), + } +} + +fn build_partition_bound_tuple(route_desc: &TupleBinaryDesc, values: &[Vec]) -> RS> { + if route_desc.field_count() != values.len() { + return Err(m_error!(EC::TupleErr, "partition bound width mismatch")); + } + let mut binaries = Vec::with_capacity(values.len()); + for (index, textual) in values.iter().enumerate() { + let field_desc = route_desc.get_field_desc(index); + let dat_type = field_desc.type_obj(); + binaries.push(textual_to_binary(dat_type.dat_type_id(), dat_type, textual)?); + } + build_tuple(&binaries, route_desc) +} + +fn textual_to_binary(data_type_id: DatTypeID, dat_type: &mudu_type::dat_type::DatType, raw: &[u8]) -> RS> { + let text = String::from_utf8(raw.to_vec()) + .map_err(|e| m_error!(EC::DecodeErr, "partition bound text is not utf8", e))?; + let normalized = strip_text_literal_quotes(text.trim()); + let datum: Box = match data_type_id { + DatTypeID::I32 => Box::new(::from_textual(&normalized)?), + DatTypeID::I64 => Box::new(::from_textual(&normalized)?), + DatTypeID::I128 => Box::new(::from_textual(&normalized)?), + DatTypeID::U128 => Box::new(::from_textual(&normalized)?), + DatTypeID::F32 => Box::new(::from_textual(&normalized)?), + DatTypeID::F64 => Box::new(::from_textual(&normalized)?), + DatTypeID::String => { + Box::new(::from_textual(&normalized)?) + } + _ => { + return Err(m_error!( + EC::NotImplemented, + format!("partition bound type {:?} is not supported", data_type_id) + )); + } + }; + datum.to_binary(dat_type).map(|binary| binary.into()) +} + +fn strip_text_literal_quotes(input: &str) -> String { + if input.len() >= 2 && input.starts_with('\'') && input.ends_with('\'') { + input[1..input.len() - 1].to_string() + } else { + input.to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contract::schema_column::SchemaColumn; + use crate::contract::schema_table::SchemaTable; + use crate::contract::table_info::TableInfo; + use async_trait::async_trait; + use mudu::error::ec::EC; + use mudu_type::dt_info::DTInfo; + + struct TestMetaMgr { + table_desc: Arc, + } + + #[async_trait] + impl MetaMgr for TestMetaMgr { + async fn get_table_by_id(&self, oid: OID) -> RS> { + if self.table_desc.id() == oid { + Ok(self.table_desc.clone()) + } else { + Err(m_error!(EC::NoSuchElement, format!("no such table {}", oid))) + } + } + + async fn get_table_by_name(&self, name: &String) -> RS>> { + Ok((self.table_desc.name() == name).then(|| self.table_desc.clone())) + } + + async fn create_table(&self, _schema: &SchemaTable) -> RS<()> { + Ok(()) + } + + async fn drop_table(&self, _table_id: OID) -> RS<()> { + Ok(()) + } + } + + fn test_table_desc() -> Arc { + TableInfo::new(SchemaTable::new( + "t".to_string(), + vec![ + SchemaColumn::new( + "id".to_string(), + DatTypeID::I32, + DTInfo::from_text(DatTypeID::I32, String::new()), + ), + SchemaColumn::new( + "v".to_string(), + DatTypeID::I32, + DTInfo::from_text(DatTypeID::I32, String::new()), + ), + ], + vec![0], + vec![1], + )) + .unwrap() + .table_desc() + .unwrap() + } + + #[tokio::test(flavor = "current_thread")] + async fn unpartitioned_table_routes_to_default_global_partition() { + let table_desc = test_table_desc(); + let router = PartitionRouter::new(Arc::new(TestMetaMgr { + table_desc: table_desc.clone(), + })); + let point = router + .route_exact_partition( + table_desc.id(), + table_desc.as_ref(), + &VecDatum::new(vec![(0, 1_i32.to_be_bytes().to_vec())]), + ) + .await + .unwrap(); + assert_eq!(point, Some(DEFAULT_UNPARTITIONED_TABLE_PARTITION_ID)); + + let range = router + .route_range_partitions( + table_desc.id(), + table_desc.as_ref(), + &Bound::Included(vec![(0, 1_i32.to_be_bytes().to_vec())]), + &Bound::Excluded(vec![(0, 2_i32.to_be_bytes().to_vec())]), + ) + .await + .unwrap(); + assert_eq!(range, Some(vec![DEFAULT_UNPARTITIONED_TABLE_PARTITION_ID])); + } +} diff --git a/mudu_kernel/src/server/partition_rpc.rs b/mudu_kernel/src/server/partition_rpc.rs new file mode 100644 index 0000000..9db88fe --- /dev/null +++ b/mudu_kernel/src/server/partition_rpc.rs @@ -0,0 +1,53 @@ +use mudu::common::id::{AttrIndex, OID}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum RpcBound { + Included(Vec), + Excluded(Vec), + Unbounded, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum PartitionRpcRequest { + ReadKey { + table_id: OID, + partition_id: OID, + key: Vec, + select: Vec, + }, + ReadRange { + table_id: OID, + partition_id: OID, + start: RpcBound, + end: RpcBound, + select: Vec, + }, + Insert { + table_id: OID, + partition_id: OID, + key: Vec, + value: Vec, + }, + Delete { + table_id: OID, + partition_id: OID, + key: Vec, + }, + Update { + table_id: OID, + partition_id: OID, + key: Vec, + values: Vec<(AttrIndex, Vec)>, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum PartitionRpcResponse { + ReadKey(Option>>), + ReadRange(Vec>>), + Insert, + Delete(usize), + Update(usize), + Err(String), +} diff --git a/mudu_kernel/src/server/request_ctx.rs b/mudu_kernel/src/server/request_ctx.rs index bf39579..02b8f54 100644 --- a/mudu_kernel/src/server/request_ctx.rs +++ b/mudu_kernel/src/server/request_ctx.rs @@ -5,12 +5,11 @@ use mudu::m_error; use mudu_contract::database::result_set::ResultSetAsync; use mudu_contract::protocol::{ encode_get_response, encode_procedure_invoke_response, encode_put_response, - encode_range_scan_response, encode_session_close_response, encode_session_create_response, - encode_server_response, GetResponse, KeyValue, ProcedureInvokeResponse, PutResponse, - RangeScanResponse, ServerResponse, - SessionCloseResponse, SessionCreateResponse, + encode_range_scan_response, encode_server_response, encode_session_close_response, + encode_session_create_response, GetResponse, KeyValue, ProcedureInvokeResponse, PutResponse, + RangeScanResponse, ServerResponse, SessionCloseResponse, SessionCreateResponse, }; -use mudu_type::datum::DatumDyn; +use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; use std::sync::Arc; use crate::server::async_func_task::HandleResult; @@ -151,7 +150,7 @@ impl RequestCtx { .worker .execute(oid, Box::new(sql.to_string()), Box::new(())) .await?; - let response = ServerResponse::new(Vec::new(), Vec::new(), affected_rows, None); + let response = ServerResponse::new(TupleFieldDesc::new(Vec::new()), Vec::new(), affected_rows, None); self.encode_server_response(response) } @@ -166,7 +165,7 @@ impl RequestCtx { .worker .batch(oid, Box::new(sql.to_string()), Box::new(())) .await?; - let response = ServerResponse::new(Vec::new(), Vec::new(), affected_rows, None); + let response = ServerResponse::new(TupleFieldDesc::new(Vec::new()), Vec::new(), affected_rows, None); self.encode_server_response(response) } @@ -214,23 +213,14 @@ impl RequestCtx { } async fn query_response(result_set: Arc) -> RS { - let desc = result_set.desc(); - let columns = desc - .fields() - .iter() - .map(|field| field.name().to_string()) - .collect(); + let desc = result_set.desc().clone(); let mut rows = Vec::new(); while let Some(row) = result_set.next().await? { if row.values().len() != desc.fields().len() { return Err(m_error!(EC::FatalError, "non consistent column number")); } - let mut values = Vec::with_capacity(row.values().len()); - for (value, field_desc) in row.values().iter().zip(desc.fields().iter()) { - values.push(value.to_textual(field_desc.dat_type())?.into()); - } - rows.push(values); + rows.push(row); } - Ok(ServerResponse::new(columns, rows, 0, None)) + Ok(ServerResponse::new(desc, rows, 0, None)) } } diff --git a/mudu_kernel/src/server/routing.rs b/mudu_kernel/src/server/routing.rs index 7681a4b..5eae326 100644 --- a/mudu_kernel/src/server/routing.rs +++ b/mudu_kernel/src/server/routing.rs @@ -1,4 +1,4 @@ -use crate::server::fsm::ConnectionState; +use crate::server::connection_state::ConnectionState; use crate::server::worker_registry::WorkerRegistry; use mudu::common::id::OID; use mudu::common::result::RS; diff --git a/mudu_kernel/src/server/server.rs b/mudu_kernel/src/server/server.rs index aacbab6..bcab20c 100644 --- a/mudu_kernel/src/server/server.rs +++ b/mudu_kernel/src/server/server.rs @@ -193,7 +193,7 @@ struct TransferredConnection { struct WorkerConnection { conn_id: u64, - state: crate::server::fsm::ConnectionState, + state: crate::server::connection_state::ConnectionState, stream: TcpStream, remote_addr: SocketAddr, transferred: bool, @@ -225,7 +225,7 @@ fn apply_handle_result_to_connection( Some(transfer.action()), )?; connection.transferred = true; - connection.state = crate::server::fsm::ConnectionState::Closing; + connection.state = crate::server::connection_state::ConnectionState::Closing; connection.write_buf.clear(); } } @@ -585,7 +585,7 @@ fn enqueue_transfer( transfer: ConnectionTransfer::new( conn_id, target_worker, - crate::server::fsm::ConnectionState::Accepted, + crate::server::connection_state::ConnectionState::Accepted, remote_addr, ), stream, @@ -644,7 +644,7 @@ fn register_connection( conn_id, WorkerConnection { conn_id, - state: crate::server::fsm::ConnectionState::Active, + state: crate::server::connection_state::ConnectionState::Active, stream, remote_addr, transferred: false, @@ -672,7 +672,7 @@ fn drive_connections( progressed |= flush_pending_writes(connection)?; let connection_progress = read_and_dispatch(worker, async_funcs, connection, inboxes)?; progressed |= connection_progress; - if connection.state == crate::server::fsm::ConnectionState::Closing + if connection.state == crate::server::connection_state::ConnectionState::Closing && connection.write_buf.is_empty() { closed.push((conn_id, connection.transferred)); @@ -693,7 +693,7 @@ fn flush_pending_writes(connection: &mut WorkerConnection) -> RS { while !connection.write_buf.is_empty() { match connection.stream.write(&connection.write_buf) { Ok(0) => { - connection.state = crate::server::fsm::ConnectionState::Closing; + connection.state = crate::server::connection_state::ConnectionState::Closing; break; } Ok(written) => { @@ -718,7 +718,7 @@ fn read_and_dispatch( loop { match connection.stream.read(&mut buf) { Ok(0) => { - connection.state = crate::server::fsm::ConnectionState::Closing; + connection.state = crate::server::connection_state::ConnectionState::Closing; break; } Ok(read) => { diff --git a/mudu_kernel/src/server/server_iouring.rs b/mudu_kernel/src/server/server_iouring.rs index 7735b82..a367431 100644 --- a/mudu_kernel/src/server/server_iouring.rs +++ b/mudu_kernel/src/server/server_iouring.rs @@ -14,7 +14,6 @@ use std::os::fd::{IntoRawFd, RawFd}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use tracing::debug; - pub(crate) struct RecoveryCoordinator { total_workers: usize, state: Mutex, @@ -69,7 +68,6 @@ pub(crate) fn sync_serve_iouring(mut cfg: IoUringTcpServerConfig, stop: Waiter) mailbox.push(WorkerMailboxMsg::Shutdown); notify_mailbox_fd(fd)?; } - debug!("notify shutdown"); Ok(()) })?; @@ -317,7 +315,7 @@ mod tests { ConnectionTransfer::new( 11, 1, - crate::server::fsm::ConnectionState::Accepted, + crate::server::connection_state::ConnectionState::Accepted, "127.0.0.1:9527".parse().unwrap(), ), -1, diff --git a/mudu_kernel/src/server/session_bound_worker_runtime.rs b/mudu_kernel/src/server/session_bound_worker_runtime.rs index b69316c..88f6bd1 100644 --- a/mudu_kernel/src/server/session_bound_worker_runtime.rs +++ b/mudu_kernel/src/server/session_bound_worker_runtime.rs @@ -1,10 +1,11 @@ +use crate::contract::meta_mgr::MetaMgr; +use crate::server::message_bus_api::{message_bus_for_worker, MessageBusRef}; use crate::server::request_response_worker::{RequestResponseWorker, WorkerRuntimeRef}; use crate::server::routing::{SessionOpenConfig, SessionOpenTransferAction}; use crate::server::worker::IoUringWorker; use crate::server::worker_local::{WorkerExecute, WorkerLocal, WorkerLocalRef}; use crate::server::worker_registry::WorkerRegistry; use crate::server::worker_snapshot::KvItem; -use crate::contract::meta_mgr::MetaMgr; use async_trait::async_trait; use mudu::common::id::OID; use mudu::common::result::RS; @@ -47,6 +48,10 @@ impl WorkerLocal for SessionBoundWorkerRuntime { self.worker.meta_mgr() } + fn message_bus(&self) -> MessageBusRef { + message_bus_for_worker(self.worker.worker_id()).expect("message bus is not registered") + } + async fn open_async(&self) -> RS { self.worker.open_session(self.current_session_id) } @@ -94,7 +99,9 @@ impl WorkerLocal for SessionBoundWorkerRuntime { start_key: &[u8], end_key: &[u8], ) -> RS> { - self.worker.range_in_session(session_id, start_key, end_key).await + self.worker + .range_in_session(session_id, start_key, end_key) + .await } async fn query( @@ -106,21 +113,11 @@ impl WorkerLocal for SessionBoundWorkerRuntime { self.worker.query(oid, sql, param).await } - async fn execute( - &self, - oid: OID, - sql: Box, - param: Box, - ) -> RS { + async fn execute(&self, oid: OID, sql: Box, param: Box) -> RS { self.worker.execute(oid, sql, param).await } - async fn batch( - &self, - oid: OID, - sql: Box, - param: Box, - ) -> RS { + async fn batch(&self, oid: OID, sql: Box, param: Box) -> RS { self.worker.batch(oid, sql, param).await } } diff --git a/mudu_kernel/src/server/worker.rs b/mudu_kernel/src/server/worker.rs index bb562d3..6b364b3 100644 --- a/mudu_kernel/src/server/worker.rs +++ b/mudu_kernel/src/server/worker.rs @@ -1,5 +1,5 @@ -use crate::mudu_conn::mudu_conn_core::MuduConnCore; use crate::contract::meta_mgr::MetaMgr; +use crate::mudu_conn::mudu_conn_core::MuduConnCore; use crate::server::async_func_runtime::AsyncFuncInvokerPtr; use crate::server::routing::{ route_worker, RoutingContext, RoutingMode, SessionOpenConfig, SessionOpenTransferAction, @@ -7,7 +7,10 @@ use crate::server::routing::{ use crate::server::session_bound_worker_runtime::{ as_worker_local_ref, new_session_bound_worker_runtime, }; -use crate::server::worker_local::{WorkerExecute, WorkerLocalRef}; +use crate::server::worker_local::{ + WorkerExecute, WorkerLocalRef, set_current_worker_local, try_current_worker_local, + unset_current_worker_local, +}; use crate::server::worker_registry::{WorkerIdentity, WorkerRegistry}; use crate::server::worker_session_manager::{SessionContext, WorkerSessionManager}; use crate::server::worker_snapshot::KvItem; @@ -99,6 +102,10 @@ impl IoUringWorker { ) })?; let worker_id = identity.worker_id; + let default_unpartitioned_worker_id = + registry.default_global_worker_id().ok_or_else(|| { + m_error!(EC::ParseErr, "worker registry has no default global worker") + })?; let log_layout = WorkerLogLayout::new(log_dir, worker_id, log_chunk_size)?.with_batching(log_batching); let log = ChunkedWorkerLogBackend::new_with_active_sessions( @@ -107,6 +114,8 @@ impl IoUringWorker { )?; let contract = Arc::new(IoUringXContract::with_worker_log_and_data_dir( log, + worker_id, + default_unpartitioned_worker_id, partition_id, data_dir, )); @@ -269,15 +278,14 @@ impl IoUringWorker { } fn put_in_session(&self, session_id: OID, key: Vec, value: Vec) -> RS<()> { - self.session_manager.with_session_tx(session_id, |tx_manager| { - match tx_manager { + self.session_manager + .with_session_tx(session_id, |tx_manager| match tx_manager { Some(tx_manager) => { tx_manager.put(key, value); Ok(()) } None => self.contract.worker_put(key, value), - } - }) + }) } pub(crate) async fn put_in_session_async( @@ -365,7 +373,11 @@ impl IoUringWorker { ) .await? } - None => self.contract.worker_range_scan_async(start_key, end_key).await?, + None => { + self.contract + .worker_range_scan_async(start_key, end_key) + .await? + } }; for item in base_items { merged.insert(item.key, Some(item.value)); @@ -393,6 +405,8 @@ impl IoUringWorker { self.ensure_session_owned_by_connection(conn_id, session_id)?; let worker_local = as_worker_local_ref(new_session_bound_worker_runtime(self.clone(), session_id)); + let prev_worker_local = try_current_worker_local(); + set_current_worker_local(worker_local.clone()); let result = self .invoke_procedure( session_id, @@ -400,8 +414,13 @@ impl IoUringWorker { request.procedure_parameters_owned(), worker_local, ) - .await?; - Ok(ProcedureInvokeResponse::new(result)) + .await; + if let Some(prev_worker_local) = prev_worker_local { + set_current_worker_local(prev_worker_local); + } else { + unset_current_worker_local(); + } + Ok(ProcedureInvokeResponse::new(result?)) } pub fn worker_index(&self) -> usize { @@ -432,6 +451,10 @@ impl IoUringWorker { self.contract.worker_log() } + pub(crate) fn ensure_partition_rpc_handler(&self) -> RS<()> { + self.contract.ensure_partition_rpc_handler() + } + pub fn x_contract(&self) -> Arc { self.contract.clone() } @@ -474,7 +497,8 @@ impl IoUringWorker { tx_mgr: Arc, ) -> RS { let stmt = core.parse_one(stmt.as_ref())?; - core.execute(stmt, param, tx_mgr, self.contract.clone()).await + core.execute(stmt, param, tx_mgr, self.contract.clone()) + .await } pub(crate) async fn query( @@ -987,6 +1011,7 @@ mod tests { async fn worker_storage_uses_worker_partition_id_for_relation_files() { let (log_dir, registry) = test_registry(1); let identity = registry.worker(0).cloned().unwrap(); + let worker_id = identity.worker_id; let partition_id = identity.partition_ids[0]; let _worker = IoUringWorker::new( identity, @@ -1002,13 +1027,18 @@ mod tests { let contract = IoUringXContract::with_log_and_data_dir( Arc::new(TestMetaMgr::new()), None, + worker_id, + worker_id, partition_id, log_dir.clone(), ); let schema = test_schema(); let table_id = schema.id(); let tx_mgr = contract.begin_tx().await.unwrap(); - contract.create_table(tx_mgr.clone(), &schema).await.unwrap(); + contract + .create_table(tx_mgr.clone(), &schema) + .await + .unwrap(); contract.commit_tx(tx_mgr).await.unwrap(); let key_path = TimeSeriesFile::relation_file_path(&log_dir, partition_id, table_id, 0); @@ -1122,7 +1152,10 @@ mod tests { .prepare_connection_transfer(conn_id, Some(action)) .unwrap(); assert_eq!(transferred.len(), 2); - assert!(futures::executor::block_on(source.get_for_connection(conn_id, session_a, b"k")).is_err()); + assert!( + futures::executor::block_on(source.get_for_connection(conn_id, session_a, b"k")) + .is_err() + ); target .adopt_connection_sessions(conn_id, &transferred) @@ -1137,7 +1170,8 @@ mod tests { .put_for_connection(conn_id, session_b, b"k".to_vec(), b"v".to_vec()) .unwrap(); assert_eq!( - futures::executor::block_on(target.get_for_connection(conn_id, session_b, b"k")).unwrap(), + futures::executor::block_on(target.get_for_connection(conn_id, session_b, b"k")) + .unwrap(), Some(b"v".to_vec()) ); } diff --git a/mudu_kernel/src/server/worker_local.rs b/mudu_kernel/src/server/worker_local.rs index 76383a3..5d892f4 100644 --- a/mudu_kernel/src/server/worker_local.rs +++ b/mudu_kernel/src/server/worker_local.rs @@ -1,5 +1,6 @@ -use crate::server::worker_snapshot::KvItem; use crate::contract::meta_mgr::MetaMgr; +use crate::server::message_bus_api::MessageBusRef; +use crate::server::worker_snapshot::KvItem; use async_trait::async_trait; use mudu::common::id::OID; use mudu::common::result::RS; @@ -27,6 +28,7 @@ pub enum WorkerExecute { pub trait WorkerLocal: Send + Sync { fn x_contract(&self) -> Arc; fn meta_mgr(&self) -> Arc; + fn message_bus(&self) -> MessageBusRef; async fn open_async(&self) -> RS; @@ -65,19 +67,9 @@ pub trait WorkerLocal: Send + Sync { param: Box, ) -> RS>; - async fn execute( - &self, - oid: OID, - sql: Box, - param: Box, - ) -> RS; + async fn execute(&self, oid: OID, sql: Box, param: Box) -> RS; - async fn batch( - &self, - oid: OID, - sql: Box, - param: Box, - ) -> RS; + async fn batch(&self, oid: OID, sql: Box, param: Box) -> RS; } pub type WorkerLocalRef = Arc; @@ -100,6 +92,7 @@ pub(crate) fn unset_current_worker_local() { }); } +#[allow(dead_code)] pub(crate) fn current_worker_local() -> WorkerLocalRef { CURRENT_WORKER_LOCAL.with(|slot| { // Safety: shared reads are confined to the current thread-local slot. @@ -110,3 +103,11 @@ pub(crate) fn current_worker_local() -> WorkerLocalRef { .unwrap_or_else(|| panic!("current worker local is not set")) }) } + +pub fn try_current_worker_local() -> Option { + CURRENT_WORKER_LOCAL.with(|slot| { + // Safety: shared reads are confined to the current thread-local slot. + let worker_local = unsafe { &*slot.get() }; + worker_local.as_ref().cloned() + }) +} diff --git a/mudu_kernel/src/server/worker_mailbox.rs b/mudu_kernel/src/server/worker_mailbox.rs index 91529c3..8603a05 100644 --- a/mudu_kernel/src/server/worker_mailbox.rs +++ b/mudu_kernel/src/server/worker_mailbox.rs @@ -1,7 +1,9 @@ +use crate::server::message_bus_api::Envelope; use crate::server::transferred_connection::TransferredConnection; #[derive(Debug)] pub(in crate::server) enum WorkerMailboxMsg { AdoptConnection(TransferredConnection), + BusMessage(Envelope), Shutdown, } diff --git a/mudu_kernel/src/server/worker_registry.rs b/mudu_kernel/src/server/worker_registry.rs index 4204444..cb658b5 100644 --- a/mudu_kernel/src/server/worker_registry.rs +++ b/mudu_kernel/src/server/worker_registry.rs @@ -116,6 +116,10 @@ impl WorkerRegistry { .get(&partition_id) .copied() } + + pub fn default_global_worker_id(&self) -> Option { + self.worker(0).map(|worker| worker.worker_id) + } } fn scan_worker_identities(log_dir: &Path) -> RS> { diff --git a/mudu_kernel/src/server/worker_ring_loop.rs b/mudu_kernel/src/server/worker_ring_loop.rs index 4824312..05996eb 100644 --- a/mudu_kernel/src/server/worker_ring_loop.rs +++ b/mudu_kernel/src/server/worker_ring_loop.rs @@ -11,12 +11,19 @@ use crate::server::loop_mailbox::{ use crate::server::loop_user_io::{ handle_completion as handle_user_io_completion, submit as submit_user_io, LoopUserIoCtx, }; +use crate::server::message_bus_api::{ + register_worker_message_bus, set_current_message_bus, unregister_worker_message_bus, + unset_current_message_bus, +}; +use crate::server::message_bus_runtime::WorkerMessageBus; use crate::server::server_iouring; use crate::server::server_iouring::RecoveryCoordinator; -use crate::server::session_bound_worker_runtime::{as_worker_local_ref, new_session_bound_worker_runtime}; +use crate::server::session_bound_worker_runtime::{ + as_worker_local_ref, new_session_bound_worker_runtime, +}; use crate::server::worker::IoUringWorker; -use crate::server::worker_loop_stats::WorkerLoopStats; use crate::server::worker_local::{set_current_worker_local, unset_current_worker_local}; +use crate::server::worker_loop_stats::WorkerLoopStats; use crate::server::worker_mailbox::WorkerMailboxMsg; use crate::server::worker_task::{spawn_system_worker_task, WorkerTaskFuture}; use crate::wal::worker_log::ChunkedWorkerLogBackend; @@ -60,6 +67,7 @@ pub(in crate::server) struct WorkerRingLoop { conn_id_alloc: Arc, recovery_coordinator: Arc, worker_local_ring: Arc, + message_bus: Arc, connection_task_fds: Arc>, #[allow(dead_code)] callback_registry: CallbackRegistry, @@ -106,6 +114,14 @@ impl WorkerRingLoop { }, ) }); + let worker_local_ring = Arc::new(WorkerLocalRing::new()); + let message_bus = WorkerMessageBus::new( + worker.worker_id(), + worker.registry().clone(), + mailbox_fds.clone(), + mailboxes.clone(), + worker_local_ring.clone(), + ); Ok(Self { log, worker, @@ -117,7 +133,8 @@ impl WorkerRingLoop { mailbox_fds, conn_id_alloc, recovery_coordinator, - worker_local_ring: Arc::new(WorkerLocalRing::new()), + worker_local_ring, + message_bus, connection_task_fds: Arc::new(scc::HashMap::new()), callback_registry: CallbackRegistry::new(), callback_sequence_frontiers: HashMap::new(), @@ -145,7 +162,12 @@ impl WorkerRingLoop { 0, ))); set_current_worker_ring(self.worker_local_ring.clone()); + set_current_message_bus(self.message_bus.as_ref()); + register_worker_message_bus(self.worker.worker_id(), &self.message_bus.as_ref())?; + self.worker.ensure_partition_rpc_handler()?; if let Err(err) = self.recover_worker_log() { + let _ = unregister_worker_message_bus(self.worker.worker_id()); + unset_current_message_bus(); unset_current_worker_ring(); unset_current_worker_local(); self.recovery_coordinator.worker_failed(); @@ -153,6 +175,8 @@ impl WorkerRingLoop { } self.recovery_coordinator.worker_succeeded()?; let r = self.run_service_loop(); + let _ = unregister_worker_message_bus(self.worker.worker_id()); + unset_current_message_bus(); unset_current_worker_ring(); unset_current_worker_local(); r @@ -199,7 +223,7 @@ impl WorkerRingLoop { crate::server::routing::ConnectionTransfer::new( conn_id, target_worker, - crate::server::fsm::ConnectionState::Accepted, + crate::server::connection_state::ConnectionState::Accepted, remote_addr, ), conn_fd, @@ -257,6 +281,9 @@ impl WorkerRingLoop { initial_response, )?; } + WorkerMailboxMsg::BusMessage(envelope) => { + self.message_bus.handle_incoming(envelope)?; + } WorkerMailboxMsg::Shutdown => { self.shutdown_triggered.store(true, Ordering::Relaxed); } diff --git a/mudu_kernel/src/server/worker_storage.rs b/mudu_kernel/src/server/worker_storage.rs index 2ba0ba7..12aad19 100644 --- a/mudu_kernel/src/server/worker_storage.rs +++ b/mudu_kernel/src/server/worker_storage.rs @@ -22,7 +22,7 @@ use crate::storage::relation::relation::Relation; use crate::wal::xl_batch::XLBatch; use crate::wal::xl_data_op::{XLDelete, XLInsert}; use crate::wal::xl_entry::TxOp; -use crate::x_engine::tx_mgr::TxMgr; +use crate::x_engine::tx_mgr::{PhysicalRelationId, TxMgr}; type WorkerStorageRegistry = std::collections::HashMap>>; @@ -34,30 +34,41 @@ fn storage_registry() -> &'static Mutex { #[derive(Clone, Debug)] pub(crate) struct PreparedWorkerCommit { xid: u64, - relation_rows: BTreeMap, Option>>>, + relation_rows: BTreeMap, Option>>>, kv_rows: BTreeMap, Option>>, batch: XLBatch, } pub struct WorkerStorage { mgr: Arc, - partition_id: OID, + default_partition_id: OID, relation_path: String, - relation_store: SccHashMap, + relation_store: SccHashMap, kv_store: SccHashMap, DataRow>, } impl WorkerStorage { + fn relation_id(&self, table_id: OID, partition_id: OID) -> PhysicalRelationId { + PhysicalRelationId { + table_id, + partition_id, + } + } + pub fn new(mgr: Arc, partition_id: OID, relation_path: String) -> Self { Self { mgr, - partition_id, + default_partition_id: partition_id, relation_path, relation_store: SccHashMap::new(), kv_store: SccHashMap::new(), } } + fn physical_partition_id(&self, partition_id: Option) -> OID { + partition_id.unwrap_or(self.default_partition_id) + } + pub fn register_global(self: &Arc) { let mut guard = storage_registry().lock().unwrap(); guard @@ -83,69 +94,120 @@ impl WorkerStorage { self.broadcast_drop_table(oid) } - #[allow(dead_code)] pub async fn contains_key(&self, oid: OID, key: &KeyTuple, txm: &dyn TxMgr) -> RS { - if let Some(staged) = txm.get_relation(oid, key.as_slice()) { + self.contains_key_on_partition(oid, None, key, txm).await + } + + pub async fn contains_key_on_partition( + &self, + oid: OID, + partition_id: Option, + key: &KeyTuple, + txm: &dyn TxMgr, + ) -> RS { + let relation_id = self.relation_id(oid, self.physical_partition_id(partition_id)); + if let Some(staged) = txm.get_relation(relation_id, key.as_slice()) { return Ok(staged.is_some()); } - self.read_visible_relation_exists(oid, key, &txm.snapshot()).await + self.read_visible_relation_exists(oid, partition_id, key, &txm.snapshot()) + .await } - + #[allow(dead_code)] pub async fn get(&self, oid: OID, key: &[u8], txm: &dyn TxMgr) -> RS>> { - if let Some(staged) = txm.get_relation(oid, key) { + self.get_on_partition(oid, None, key, txm).await + } + + pub async fn get_on_partition( + &self, + oid: OID, + partition_id: Option, + key: &[u8], + txm: &dyn TxMgr, + ) -> RS>> { + let relation_id = self.relation_id(oid, self.physical_partition_id(partition_id)); + if let Some(staged) = txm.get_relation(relation_id, key) { return Ok(staged); } let key = KeyTuple::from(key.to_vec()); - self.read_visible_relation_value(oid, &key, &txm.snapshot()).await + self.read_visible_relation_value(oid, partition_id, &key, &txm.snapshot()) + .await } - pub async fn put( + #[allow(dead_code)] + pub async fn put(&self, oid: OID, key: Vec, value: Vec, txm: &dyn TxMgr) -> RS<()> { + self.put_on_partition(oid, None, key, value, txm).await + } + + pub async fn put_on_partition( &self, oid: OID, + partition_id: Option, key: Vec, value: Vec, txm: &dyn TxMgr, ) -> RS<()> { let key_tuple = KeyTuple::from(key.clone()); + let relation_id = self.relation_id(oid, self.physical_partition_id(partition_id)); - self.ensure_no_relation_write_conflict(oid, &key_tuple, &txm.snapshot()) + self.ensure_no_relation_write_conflict(oid, partition_id, &key_tuple, &txm.snapshot()) .await?; - txm.put_relation(oid, key, value); + txm.put_relation(relation_id, key, value); Ok(()) } - + #[allow(dead_code)] pub async fn remove(&self, oid: OID, key: &[u8], txm: &dyn TxMgr) -> RS>> { + self.remove_on_partition(oid, None, key, txm).await + } + + pub async fn remove_on_partition( + &self, + oid: OID, + partition_id: Option, + key: &[u8], + txm: &dyn TxMgr, + ) -> RS>> { let key_tuple = KeyTuple::from(key.to_vec()); - self.ensure_no_relation_write_conflict(oid, &key_tuple, &txm.snapshot()) + let relation_id = self.relation_id(oid, self.physical_partition_id(partition_id)); + self.ensure_no_relation_write_conflict(oid, partition_id, &key_tuple, &txm.snapshot()) .await?; - let current = match txm.get_relation(oid, key) { + let current = match txm.get_relation(relation_id, key) { Some(staged) => staged, - None => self - .read_visible_relation_value(oid, &key_tuple, &txm.snapshot()) - .await?, + None => { + self.read_visible_relation_value(oid, partition_id, &key_tuple, &txm.snapshot()) + .await? + } }; if current.is_some() { - txm.delete_relation(oid, key.to_vec()); + txm.delete_relation(relation_id, key.to_vec()); } Ok(current) - } - pub async fn range( &self, oid: OID, bounds: (Bound<&[u8]>, Bound<&[u8]>), txm: &dyn TxMgr, + ) -> RS, Vec)>> { + self.range_on_partition(oid, None, bounds, txm).await + } + + pub async fn range_on_partition( + &self, + oid: OID, + partition_id: Option, + bounds: (Bound<&[u8]>, Bound<&[u8]>), + txm: &dyn TxMgr, ) -> RS, Vec)>> { let base_items = self - .range_visible_relation(oid, bounds, &txm.snapshot()) + .range_visible_relation(oid, partition_id, bounds, &txm.snapshot()) .await?; let (start_key, end_key) = bounds_to_scan(&bounds); - let staged_items = txm.staged_relation_items_in_range(oid, &start_key, &end_key); + let relation_id = self.relation_id(oid, self.physical_partition_id(partition_id)); + let staged_items = txm.staged_relation_items_in_range(relation_id, &start_key, &end_key); let mut merged = BTreeMap::new(); for (key, value) in base_items { @@ -185,7 +247,6 @@ impl WorkerStorage { .map(|version| version.tuple().clone())) } - pub async fn kv_range( &self, start_key: &[u8], @@ -225,7 +286,6 @@ impl WorkerStorage { Ok(items) } - #[allow(dead_code)] pub(crate) async fn commit_tx(&self, txm: &mut WorkerTxManager) -> RS<()> { let prepared = self.prepare_commit_async(txm).await?; @@ -286,7 +346,10 @@ impl WorkerStorage { Ok(()) } - pub(crate) async fn apply_prepared_commit_async(&self, prepared: PreparedWorkerCommit) -> RS<()> { + pub(crate) async fn apply_prepared_commit_async( + &self, + prepared: PreparedWorkerCommit, + ) -> RS<()> { self.apply_relation_rows_async(&prepared).await?; self.apply_kv_rows_async(&prepared).await?; Ok(()) @@ -296,16 +359,16 @@ impl WorkerStorage { for entry in batch.entries { for op in entry.ops { match op { - TxOp::Insert(insert) if insert.table_id == 0 => { + TxOp::Insert(insert) if insert.table_id == 0 && insert.partition_id == 0 => { self.worker_put_local(insert.key, insert.value, entry.xid)?; } - TxOp::Delete(delete) if delete.table_id == 0 => { + TxOp::Delete(delete) if delete.table_id == 0 && delete.partition_id == 0 => { self.worker_delete_local(delete.key, entry.xid)?; } TxOp::Insert(insert) => { self.apply_relation_replay_insert(insert, entry.xid)?; } - TxOp::Delete(delete) if delete.table_id != 0 => { + TxOp::Delete(delete) => { self.apply_relation_replay_delete(delete, entry.xid)?; } _ => {} @@ -327,7 +390,7 @@ impl WorkerStorage { &self, snapshot: &WorkerSnapshot, xid: u64, - relation_rows: BTreeMap, Option>>>, + relation_rows: BTreeMap, Option>>>, kv_rows: BTreeMap, Option>>, batch: XLBatch, ) -> RS { @@ -346,7 +409,7 @@ impl WorkerStorage { &self, snapshot: &WorkerSnapshot, xid: u64, - relation_rows: BTreeMap, Option>>>, + relation_rows: BTreeMap, Option>>>, kv_rows: BTreeMap, Option>>, batch: XLBatch, ) -> RS { @@ -366,21 +429,32 @@ impl WorkerStorage { &self, snapshot: &WorkerSnapshot, xid: u64, - relation_rows: &BTreeMap, Option>>>, + relation_rows: &BTreeMap, Option>>>, ) -> RS<()> { - for (oid, rows) in relation_rows { + for (relation_id, rows) in relation_rows { let relation = self .relation_store - .get_sync(oid) - .ok_or_else(|| m_error!(EC::NoSuchElement, format!("no such table {}", oid)))?; + .get_sync(relation_id) + .ok_or_else(|| { + m_error!( + EC::NoSuchElement, + format!( + "no such table {} partition {}", + relation_id.table_id, relation_id.partition_id + ) + ) + })?; for key in rows.keys() { let key_tuple = KeyTuple::from(key.clone()); - if relation.get().has_write_conflict_sync(&key_tuple, snapshot)? { + if relation + .get() + .has_write_conflict_sync(&key_tuple, snapshot)? + { return Err(m_error!( EC::TxErr, format!( - "write-write conflict on table {} key {:?} for transaction {}", - oid, key, xid + "write-write conflict on table {} partition {} key {:?} for transaction {}", + relation_id.table_id, relation_id.partition_id, key, xid ) )); } @@ -393,21 +467,33 @@ impl WorkerStorage { &self, snapshot: &WorkerSnapshot, xid: u64, - relation_rows: &BTreeMap, Option>>>, + relation_rows: &BTreeMap, Option>>>, ) -> RS<()> { - for (oid, rows) in relation_rows { + for (relation_id, rows) in relation_rows { let relation = self .relation_store - .get_sync(oid) - .ok_or_else(|| m_error!(EC::NoSuchElement, format!("no such table {}", oid)))?; + .get_sync(relation_id) + .ok_or_else(|| { + m_error!( + EC::NoSuchElement, + format!( + "no such table {} partition {}", + relation_id.table_id, relation_id.partition_id + ) + ) + })?; for key in rows.keys() { let key_tuple = KeyTuple::from(key.clone()); - if relation.get().has_write_conflict(&key_tuple, snapshot).await? { + if relation + .get() + .has_write_conflict(&key_tuple, snapshot) + .await? + { return Err(m_error!( EC::TxErr, format!( - "write-write conflict on table {} key {:?} for transaction {}", - oid, key, xid + "write-write conflict on table {} partition {} key {:?} for transaction {}", + relation_id.table_id, relation_id.partition_id, key, xid ) )); } @@ -444,11 +530,19 @@ impl WorkerStorage { } fn apply_relation_rows(&self, prepared: &PreparedWorkerCommit) -> RS<()> { - for (oid, rows) in &prepared.relation_rows { + for (relation_id, rows) in &prepared.relation_rows { let relation = self .relation_store - .get_sync(oid) - .ok_or_else(|| m_error!(EC::NoSuchElement, format!("no such table {}", oid)))?; + .get_sync(relation_id) + .ok_or_else(|| { + m_error!( + EC::NoSuchElement, + format!( + "no such table {} partition {}", + relation_id.table_id, relation_id.partition_id + ) + ) + })?; for (key, value) in rows { relation .get() @@ -459,11 +553,19 @@ impl WorkerStorage { } async fn apply_relation_rows_async(&self, prepared: &PreparedWorkerCommit) -> RS<()> { - for (oid, rows) in &prepared.relation_rows { + for (relation_id, rows) in &prepared.relation_rows { let relation = self .relation_store - .get_sync(oid) - .ok_or_else(|| m_error!(EC::NoSuchElement, format!("no such table {}", oid)))?; + .get_sync(relation_id) + .ok_or_else(|| { + m_error!( + EC::NoSuchElement, + format!( + "no such table {} partition {}", + relation_id.table_id, relation_id.partition_id + ) + ) + })?; for (key, value) in rows { relation .get() @@ -497,24 +599,32 @@ impl WorkerStorage { fn apply_relation_replay_insert(&self, insert: XLInsert, xid: u64) -> RS<()> { let relation = self .relation_store - .get_sync(&insert.table_id) + .get_sync(&self.relation_id(insert.table_id, insert.partition_id)) .ok_or_else(|| { m_error!( EC::NoSuchElement, - format!("no such table {}", insert.table_id) + format!( + "no such table {} partition {}", + insert.table_id, insert.partition_id + ) ) })?; - relation.get().write_value_sync(insert.key, insert.value, xid) + relation + .get() + .write_value_sync(insert.key, insert.value, xid) } fn apply_relation_replay_delete(&self, delete: XLDelete, xid: u64) -> RS<()> { let relation = self .relation_store - .get_sync(&delete.table_id) + .get_sync(&self.relation_id(delete.table_id, delete.partition_id)) .ok_or_else(|| { m_error!( EC::NoSuchElement, - format!("no such table {}", delete.table_id) + format!( + "no such table {} partition {}", + delete.table_id, delete.partition_id + ) ) })?; relation.get().write_delete_sync(delete.key, xid) @@ -524,12 +634,13 @@ impl WorkerStorage { async fn read_visible_relation_exists( &self, oid: OID, + partition_id: Option, key: &KeyTuple, snapshot: &WorkerSnapshot, ) -> RS { let relation = self .relation_store - .get_sync(&oid) + .get_sync(&self.relation_id(oid, self.physical_partition_id(partition_id))) .ok_or_else(|| m_error!(EC::NoSuchElement, format!("no such table {}", oid)))?; relation.get().has_visible_version(key, snapshot).await } @@ -537,12 +648,14 @@ impl WorkerStorage { async fn read_visible_relation_value( &self, oid: OID, + partition_id: Option, key: &KeyTuple, snapshot: &WorkerSnapshot, ) -> RS>> { + self.ensure_relation_index(oid, partition_id).await?; let relation = self .relation_store - .get_sync(&oid) + .get_sync(&self.relation_id(oid, self.physical_partition_id(partition_id))) .ok_or_else(|| m_error!(EC::NoSuchElement, format!("no such table {}", oid)))?; relation.get().visible_value(key, snapshot).await } @@ -550,12 +663,14 @@ impl WorkerStorage { async fn range_visible_relation( &self, oid: OID, + partition_id: Option, bounds: (Bound<&[u8]>, Bound<&[u8]>), snapshot: &WorkerSnapshot, ) -> RS, Vec)>> { + self.ensure_relation_index(oid, partition_id).await?; let relation = self .relation_store - .get_sync(&oid) + .get_sync(&self.relation_id(oid, self.physical_partition_id(partition_id))) .ok_or_else(|| m_error!(EC::NoSuchElement, format!("no such table {}", oid)))?; relation.get().visible_range(bounds, snapshot).await } @@ -563,12 +678,14 @@ impl WorkerStorage { async fn ensure_no_relation_write_conflict( &self, oid: OID, + partition_id: Option, key: &KeyTuple, snapshot: &WorkerSnapshot, ) -> RS<()> { + self.ensure_relation_index(oid, partition_id).await?; let relation = self .relation_store - .get_sync(&oid) + .get_sync(&self.relation_id(oid, self.physical_partition_id(partition_id))) .ok_or_else(|| m_error!(EC::NoSuchElement, format!("no such table {}", oid)))?; if relation.get().has_write_conflict(key, snapshot).await? { return Err(m_error!( @@ -585,11 +702,20 @@ impl WorkerStorage { } fn create_relation_index(&self, oid: OID, table_desc: &TableDesc) -> RS<()> { + self.create_relation_index_for_partition(oid, self.default_partition_id, table_desc) + } + + fn create_relation_index_for_partition( + &self, + oid: OID, + partition_id: OID, + table_desc: &TableDesc, + ) -> RS<()> { let _ = self.relation_store.insert_sync( - oid, + self.relation_id(oid, partition_id), Relation::new( oid, - self.partition_id, + partition_id, self.relation_path.clone(), table_desc, ), @@ -597,6 +723,18 @@ impl WorkerStorage { Ok(()) } + async fn ensure_relation_index(&self, oid: OID, partition_id: Option) -> RS<()> { + let partition_id = self.physical_partition_id(partition_id); + if self + .relation_store + .contains_sync(&self.relation_id(oid, partition_id)) + { + return Ok(()); + } + let table_desc = self.mgr.get_table_by_id(oid).await?; + self.create_relation_index_for_partition(oid, partition_id, table_desc.as_ref()) + } + fn apply_create_table_local(&self, schema: &SchemaTable) -> RS<()> { let table_desc = crate::contract::table_info::TableInfo::new(schema.clone())?.table_desc()?; @@ -604,7 +742,9 @@ impl WorkerStorage { } fn apply_drop_table_local(&self, oid: OID) { - let _ = self.relation_store.remove_sync(&oid); + let _ = self + .relation_store + .remove_sync(&self.relation_id(oid, self.default_partition_id)); } fn broadcast_create_table(&self, schema: &SchemaTable) -> RS<()> { @@ -856,8 +996,7 @@ mod tests { assert!(futures::executor::block_on(mgr.get_table_by_id(oid)).is_err()); let mut tx = begin_tx(2, vec![]); - let err = block_on(storage2.put(oid, i32_bytes(8), i32_bytes(80), &mut tx)) - .unwrap_err(); + let err = block_on(storage2.put(oid, i32_bytes(8), i32_bytes(80), &mut tx)).unwrap_err(); assert!(format!("{err}").contains("no such table")); } @@ -907,16 +1046,14 @@ mod tests { block_on(storage.put(oid, i32_bytes(2), i32_bytes(20), &mut new_tx)).unwrap(); block_on(storage.commit_tx(&mut new_tx)).unwrap(); - let rows = block_on(storage - .range( - oid, - ( - Bound::Included(i32_bytes(1).as_slice()), - Bound::Included(i32_bytes(9).as_slice()), - ), - &mut old_tx, - ) - ) + let rows = block_on(storage.range( + oid, + ( + Bound::Included(i32_bytes(1).as_slice()), + Bound::Included(i32_bytes(9).as_slice()), + ), + &mut old_tx, + )) .unwrap(); assert_eq!(rows, vec![(i32_bytes(1), i32_bytes(10))]); } @@ -984,7 +1121,10 @@ mod tests { block_on(storage.kv_get(b"a", Some(&snapshot))).unwrap(), Some(b"0".to_vec()) ); - assert_eq!(block_on(storage.kv_get(b"a", None)).unwrap(), Some(b"1".to_vec())); + assert_eq!( + block_on(storage.kv_get(b"a", None)).unwrap(), + Some(b"1".to_vec()) + ); } #[test] @@ -1034,8 +1174,14 @@ mod tests { storage.apply_prepared_commit(prepared1).unwrap(); storage.apply_prepared_commit(prepared2).unwrap(); - assert_eq!(block_on(storage.kv_get(b"a", None)).unwrap(), Some(b"1".to_vec())); - assert_eq!(block_on(storage.kv_get(b"b", None)).unwrap(), Some(b"2".to_vec())); + assert_eq!( + block_on(storage.kv_get(b"a", None)).unwrap(), + Some(b"1".to_vec()) + ); + assert_eq!( + block_on(storage.kv_get(b"b", None)).unwrap(), + Some(b"2".to_vec()) + ); } #[test] @@ -1048,12 +1194,14 @@ mod tests { TxOp::Begin, TxOp::Insert(XLInsert { table_id: 0, + partition_id: 0, tuple_id: 0, key: b"k".to_vec(), value: b"v".to_vec(), }), TxOp::Insert(XLInsert { table_id: oid, + partition_id: 0, tuple_id: 0, key: i32_bytes(7), value: i32_bytes(70), @@ -1065,7 +1213,10 @@ mod tests { storage.replay_batch(batch).unwrap(); - assert_eq!(block_on(storage.kv_get(b"k", None)).unwrap(), Some(b"v".to_vec())); + assert_eq!( + block_on(storage.kv_get(b"k", None)).unwrap(), + Some(b"v".to_vec()) + ); let mut tx = begin_tx(10, vec![]); assert_eq!( block_on(storage.get(oid, &i32_bytes(7), &mut tx)).unwrap(), @@ -1087,6 +1238,7 @@ mod tests { TxOp::Begin, TxOp::Delete(XLDelete { table_id: 0, + partition_id: 0, tuple_id: 0, key: b"k".to_vec(), }), diff --git a/mudu_kernel/src/server/worker_tx_manager.rs b/mudu_kernel/src/server/worker_tx_manager.rs index 4db55cd..0369527 100644 --- a/mudu_kernel/src/server/worker_tx_manager.rs +++ b/mudu_kernel/src/server/worker_tx_manager.rs @@ -2,15 +2,14 @@ use crate::server::worker_snapshot::WorkerSnapshot; use crate::wal::xl_batch::XLBatch; use crate::wal::xl_data_op::{XLDelete, XLInsert}; use crate::wal::xl_entry::{TxOp, XLEntry}; -use crate::x_engine::tx_mgr::TxMgr; -use mudu::common::id::OID; +use crate::x_engine::tx_mgr::{PhysicalRelationId, TxMgr}; use std::collections::BTreeMap; use std::sync::Mutex; struct WorkerTxState { staged_puts: BTreeMap, Option>>, - staged_relation_ops: BTreeMap, Option>>>, - write_ops: Vec<(OID, Vec)>, + staged_relation_ops: BTreeMap, Option>>>, + write_ops: Vec<(PhysicalRelationId, Vec)>, log_buffer: Vec, } @@ -47,6 +46,7 @@ impl TxMgr for WorkerTxManager { state.staged_puts.insert(key.clone(), Some(value.clone())); state.log_buffer.push(TxOp::Insert(XLInsert { table_id: 0, + partition_id: 0, tuple_id: 0, key, value, @@ -58,6 +58,7 @@ impl TxMgr for WorkerTxManager { state.staged_puts.insert(key.clone(), None); state.log_buffer.push(TxOp::Delete(XLDelete { table_id: 0, + partition_id: 0, tuple_id: 0, key, })); @@ -68,53 +69,55 @@ impl TxMgr for WorkerTxManager { state.staged_puts.get(key).cloned() } - fn put_relation(&self, oid: OID, key: Vec, value: Vec) { + fn put_relation(&self, relation_id: PhysicalRelationId, key: Vec, value: Vec) { let mut state = self.state.lock().unwrap(); state .staged_relation_ops - .entry(oid) + .entry(relation_id) .or_default() .insert(key.clone(), Some(value.clone())); state.log_buffer.push(TxOp::Insert(XLInsert { - table_id: oid, + table_id: relation_id.table_id, + partition_id: relation_id.partition_id, tuple_id: 0, key, value, })); } - fn delete_relation(&self, oid: OID, key: Vec) { + fn delete_relation(&self, relation_id: PhysicalRelationId, key: Vec) { let mut state = self.state.lock().unwrap(); state .staged_relation_ops - .entry(oid) + .entry(relation_id) .or_default() .insert(key.clone(), None); state.log_buffer.push(TxOp::Delete(XLDelete { - table_id: oid, + table_id: relation_id.table_id, + partition_id: relation_id.partition_id, tuple_id: 0, key, })); } - fn get_relation(&self, oid: OID, key: &[u8]) -> Option>> { + fn get_relation(&self, relation_id: PhysicalRelationId, key: &[u8]) -> Option>> { let state = self.state.lock().unwrap(); state .staged_relation_ops - .get(&oid) + .get(&relation_id) .and_then(|rows| rows.get(key).cloned()) } fn staged_relation_items_in_range( &self, - oid: OID, + relation_id: PhysicalRelationId, start_key: &[u8], end_key: &[u8], ) -> Vec<(Vec, Option>)> { let state = self.state.lock().unwrap(); state .staged_relation_ops - .get(&oid) + .get(&relation_id) .map(|rows| { rows.iter() .filter(|(key, _)| is_key_in_range(key, start_key, end_key)) @@ -124,7 +127,9 @@ impl TxMgr for WorkerTxManager { .unwrap_or_default() } - fn staged_relation_ops(&self) -> BTreeMap, Option>>> { + fn staged_relation_ops( + &self, + ) -> BTreeMap, Option>>> { let state = self.state.lock().unwrap(); state.staged_relation_ops.clone() } @@ -153,7 +158,7 @@ impl TxMgr for WorkerTxManager { state.staged_puts.is_empty() && state.staged_relation_ops.is_empty() } - fn write_ops(&self) -> Vec<(OID, Vec)> { + fn write_ops(&self) -> Vec<(PhysicalRelationId, Vec)> { let state = self.state.lock().unwrap(); state.write_ops.clone() } @@ -163,11 +168,17 @@ impl TxMgr for WorkerTxManager { state.write_ops.clear(); let mut write_ops = Vec::new(); for key in state.staged_puts.keys() { - write_ops.push((0, key.clone())); + write_ops.push(( + PhysicalRelationId { + table_id: 0, + partition_id: 0, + }, + key.clone(), + )); } - for (oid, ops) in &state.staged_relation_ops { + for (relation_id, ops) in &state.staged_relation_ops { for key in ops.keys() { - write_ops.push((*oid, key.clone())); + write_ops.push((*relation_id, key.clone())); } } state.write_ops = write_ops; diff --git a/mudu_kernel/src/server/x_contract.rs b/mudu_kernel/src/server/x_contract.rs index b459ff2..323833a 100644 --- a/mudu_kernel/src/server/x_contract.rs +++ b/mudu_kernel/src/server/x_contract.rs @@ -8,13 +8,23 @@ use mudu::m_error; use mudu_contract::tuple::build_tuple::build_tuple; use mudu_contract::tuple::tuple_binary::TupleBinary as TupleRaw; use mudu_contract::tuple::update_tuple::update_tuple; +use mudu_type::dt_function::send_binary; use std::ops::Bound; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use crate::contract::meta_mgr::MetaMgr; use crate::contract::schema_table::SchemaTable; use crate::contract::table_desc::TableDesc; use crate::meta::meta_mgr_factory::MetaMgrFactory; +use crate::server::message_bus_api::{ + current_message_bus, DeliveryMode, EndpointId, Envelope, MessageKind, OutgoingMessage, + RecvFilter, +}; +use crate::server::partition_rpc::{PartitionRpcRequest, PartitionRpcResponse, RpcBound}; +use crate::server::partition_router::{ + PartitionRouter, DEFAULT_UNPARTITIONED_TABLE_PARTITION_ID, +}; use crate::server::worker_snapshot::{KvItem, WorkerSnapshot, WorkerSnapshotMgr}; use crate::server::worker_storage::WorkerStorage; use crate::server::worker_tx_manager::WorkerTxManager; @@ -27,10 +37,16 @@ use crate::x_engine::api::{ }; use crate::x_engine::tx_mgr::TxMgr; type DatBin = Buf; +const PARTITION_RPC_REQUEST_KIND: MessageKind = MessageKind::User(0x7101); +const PARTITION_RPC_RESPONSE_KIND: MessageKind = MessageKind::User(0x7102); pub struct IoUringXContract { + worker_id: OID, + default_unpartitioned_worker_id: OID, meta_mgr: Arc, storage: Arc, + partition_router: PartitionRouter, + partition_rpc_registered: AtomicBool, log: Option, snapshot_mgr: WorkerSnapshotMgr, tx_lock: XLockMgr, @@ -48,16 +64,18 @@ struct VecCursorInner { impl IoUringXContract { pub fn new(meta_mgr: Arc) -> Self { - Self::with_log_and_data_dir(meta_mgr, None, 0, default_worker_storage_data_dir()) + Self::with_log_and_data_dir(meta_mgr, None, 0, 0, 0, default_worker_storage_data_dir()) } pub fn with_log(meta_mgr: Arc, log: Option) -> Self { - Self::with_log_and_data_dir(meta_mgr, log, 0, default_worker_storage_data_dir()) + Self::with_log_and_data_dir(meta_mgr, log, 0, 0, 0, default_worker_storage_data_dir()) } pub fn with_log_and_data_dir( meta_mgr: Arc, log: Option, + worker_id: OID, + default_unpartitioned_worker_id: OID, partition_id: OID, data_dir: String, ) -> Self { @@ -67,8 +85,12 @@ impl IoUringXContract { .bootstrap_existing_tables_sync() .unwrap_or_else(|e| panic!("bootstrap worker storage from meta failed: {e}")); Self { + worker_id, + default_unpartitioned_worker_id, meta_mgr: meta_mgr.clone(), storage, + partition_router: PartitionRouter::new(meta_mgr.clone()), + partition_rpc_registered: AtomicBool::new(false), log, snapshot_mgr: WorkerSnapshotMgr::default(), tx_lock: XLockMgr::new(), @@ -76,23 +98,47 @@ impl IoUringXContract { } pub fn with_worker_log(log: ChunkedWorkerLogBackend) -> Self { - Self::with_worker_log_and_data_dir(log, 0, default_worker_storage_data_dir()) + Self::with_worker_log_and_data_dir(log, 0, 0, 0, default_worker_storage_data_dir()) } pub fn with_worker_log_and_data_dir( log: ChunkedWorkerLogBackend, + worker_id: OID, + default_unpartitioned_worker_id: OID, partition_id: OID, data_dir: String, ) -> Self { let meta_mgr = MetaMgrFactory::create(data_dir.clone()) .unwrap_or_else(|e| panic!("create worker meta manager failed: {e}")); - Self::with_log_and_data_dir(meta_mgr, Some(log.clone()), partition_id, data_dir) + Self::with_log_and_data_dir( + meta_mgr, + Some(log.clone()), + worker_id, + default_unpartitioned_worker_id, + partition_id, + data_dir, + ) } pub fn worker_log(&self) -> Option { self.log.clone() } + pub fn worker_id(&self) -> OID { + self.worker_id + } + + async fn resolve_partition_worker(&self, partition_id: OID) -> RS> { + match self.meta_mgr.get_partition_worker(partition_id).await? { + Some(worker_id) => Ok(Some(worker_id)), + None if partition_id == DEFAULT_UNPARTITIONED_TABLE_PARTITION_ID => { + Ok((self.default_unpartitioned_worker_id != 0) + .then_some(self.default_unpartitioned_worker_id)) + } + None => Ok(None), + } + } + pub fn worker_begin_tx(&self) -> RS> { Ok(Arc::new(WorkerTxManager::new(self.snapshot_mgr.begin_tx()))) } @@ -343,7 +389,9 @@ impl IoUringXContract { }; let result = async { if let Some(log) = log { - new_xl_batch_writer(log.clone()).append(prepared.batch()).await?; + new_xl_batch_writer(log.clone()) + .append(prepared.batch()) + .await?; log.flush_async().await?; } storage.apply_prepared_commit_async(prepared).await?; @@ -363,6 +411,26 @@ impl IoUringXContract { } self.storage.replay_batch(batch) } + + pub fn ensure_partition_rpc_handler(self: &Arc) -> RS<()> { + if self.worker_id == 0 || self.partition_rpc_registered.swap(true, Ordering::SeqCst) { + return Ok(()); + } + let bus = current_message_bus()?; + let contract = self.clone(); + bus.on_recv_callback( + RecvFilter { + dst: Some(EndpointId::Worker(self.worker_id)), + kind: Some(PARTITION_RPC_REQUEST_KIND), + ..RecvFilter::default() + }, + Arc::new(move |envelope| { + let contract = contract.clone(); + Box::pin(async move { contract.handle_partition_rpc(envelope).await }) + }), + )?; + Ok(()) + } } fn default_worker_storage_data_dir() -> String { @@ -376,12 +444,290 @@ fn default_worker_storage_data_dir() -> String { } impl IoUringXContract { + async fn handle_partition_rpc(&self, envelope: Envelope) -> RS<()> { + let request = rmp_serde::from_slice::(envelope.payload()).map_err(|e| { + m_error!(EC::DecodeErr, "decode partition rpc request error", e) + })?; + let response = match self.execute_partition_rpc(request).await { + Ok(response) => response, + Err(err) => PartitionRpcResponse::Err(err.to_string()), + }; + let payload = rmp_serde::to_vec(&response) + .map_err(|e| m_error!(EC::EncodeErr, "encode partition rpc response error", e))?; + let bus = current_message_bus()?; + block_on(bus.send( + envelope.src().clone(), + OutgoingMessage::new(PARTITION_RPC_RESPONSE_KIND, payload) + .with_correlation_id(envelope.msg_id()) + .with_delivery(DeliveryMode::Response), + ))?; + Ok(()) + } + + async fn execute_partition_rpc(&self, request: PartitionRpcRequest) -> RS { + match request { + PartitionRpcRequest::ReadKey { + table_id, + partition_id, + key, + select, + } => { + let desc = self.meta_mgr.get_table_by_id(table_id).await?; + let tx_mgr = self.worker_begin_tx()?; + let opt_value = self + .storage + .get_on_partition(table_id, Some(partition_id), &key, tx_mgr.as_ref()) + .await?; + self.worker_rollback_tx(tx_mgr)?; + let projected = opt_value + .map(|value| { + project_selected_fields(&desc, &key, &value, &VecSelTerm::new(select)) + }) + .transpose()?; + Ok(PartitionRpcResponse::ReadKey(projected)) + } + PartitionRpcRequest::ReadRange { + table_id, + partition_id, + start, + end, + select, + } => { + let desc = self.meta_mgr.get_table_by_id(table_id).await?; + let tx_mgr = self.worker_begin_tx()?; + let rows = self + .storage + .range_on_partition( + table_id, + Some(partition_id), + (rpc_bound_as_ref(&start), rpc_bound_as_ref(&end)), + tx_mgr.as_ref(), + ) + .await?; + self.worker_rollback_tx(tx_mgr)?; + let mut projected = Vec::with_capacity(rows.len()); + for (key, value) in rows { + projected.push(project_selected_fields( + &desc, + &key, + &value, + &VecSelTerm::new(select.clone()), + )?); + } + Ok(PartitionRpcResponse::ReadRange(projected)) + } + PartitionRpcRequest::Insert { + table_id, + partition_id, + key, + value, + } => { + let tx_mgr = self.worker_begin_tx()?; + let current = self + .storage + .get_on_partition(table_id, Some(partition_id), &key, tx_mgr.as_ref()) + .await?; + if current.is_some() { + self.worker_rollback_tx(tx_mgr)?; + return Err(m_error!(EC::ExistingSuchElement, "existing key")); + } + self.storage + .put_on_partition(table_id, Some(partition_id), key, value, tx_mgr.as_ref()) + .await?; + self.worker_commit_tx_async(tx_mgr).await?; + Ok(PartitionRpcResponse::Insert) + } + PartitionRpcRequest::Delete { + table_id, + partition_id, + key, + } => { + let tx_mgr = self.worker_begin_tx()?; + let deleted = self + .storage + .remove_on_partition(table_id, Some(partition_id), &key, tx_mgr.as_ref()) + .await?; + self.worker_commit_tx_async(tx_mgr).await?; + Ok(PartitionRpcResponse::Delete(usize::from(deleted.is_some()))) + } + PartitionRpcRequest::Update { + table_id, + partition_id, + key, + values, + } => { + let desc = self.meta_mgr.get_table_by_id(table_id).await?; + let tx_mgr = self.worker_begin_tx()?; + let current = self + .storage + .get_on_partition(table_id, Some(partition_id), &key, tx_mgr.as_ref()) + .await?; + let Some(current) = current else { + self.worker_rollback_tx(tx_mgr)?; + return Ok(PartitionRpcResponse::Update(0)); + }; + let updated = apply_value_update(¤t, &VecDatum::new(values), &desc)?; + self.storage + .put_on_partition( + table_id, + Some(partition_id), + key, + updated, + tx_mgr.as_ref(), + ) + .await?; + self.worker_commit_tx_async(tx_mgr).await?; + Ok(PartitionRpcResponse::Update(1)) + } + } + } + + fn send_partition_rpc( + &self, + target_worker_id: OID, + request: PartitionRpcRequest, + ) -> RS { + let bus = current_message_bus()?; + let payload = rmp_serde::to_vec(&request) + .map_err(|e| m_error!(EC::EncodeErr, "encode partition rpc request error", e))?; + let msg_id = block_on(bus.send( + EndpointId::Worker(target_worker_id), + OutgoingMessage::new(PARTITION_RPC_REQUEST_KIND, payload) + .with_delivery(DeliveryMode::Request), + ))?; + let envelope = block_on(bus.recv(RecvFilter { + src: Some(EndpointId::Worker(target_worker_id)), + dst: Some(EndpointId::Worker(self.worker_id)), + kind: Some(PARTITION_RPC_RESPONSE_KIND), + correlation_id: Some(msg_id), + }))?; + rmp_serde::from_slice(envelope.payload()) + .map_err(|e| m_error!(EC::DecodeErr, "decode partition rpc response error", e)) + } + + async fn remote_read_key( + &self, + target_worker_id: OID, + table_id: OID, + partition_id: OID, + key: Vec, + select: Vec, + ) -> RS>> { + match self.send_partition_rpc( + target_worker_id, + PartitionRpcRequest::ReadKey { + table_id, + partition_id, + key, + select, + }, + )? + { + PartitionRpcResponse::ReadKey(value) => Ok(value), + PartitionRpcResponse::Err(err) => Err(m_error!(EC::InternalErr, err)), + _ => Err(m_error!(EC::InternalErr, "unexpected read_key rpc response")), + } + } + + async fn remote_read_range( + &self, + target_worker_id: OID, + table_id: OID, + partition_id: OID, + start: RpcBound, + end: RpcBound, + select: Vec, + ) -> RS>> { + match self.send_partition_rpc( + target_worker_id, + PartitionRpcRequest::ReadRange { + table_id, + partition_id, + start, + end, + select, + }, + )? + { + PartitionRpcResponse::ReadRange(rows) => Ok(rows), + PartitionRpcResponse::Err(err) => Err(m_error!(EC::InternalErr, err)), + _ => Err(m_error!(EC::InternalErr, "unexpected read_range rpc response")), + } + } + + async fn remote_insert( + &self, + target_worker_id: OID, + table_id: OID, + partition_id: OID, + key: Vec, + value: Vec, + ) -> RS<()> { + match self.send_partition_rpc( + target_worker_id, + PartitionRpcRequest::Insert { + table_id, + partition_id, + key, + value, + }, + )? { + PartitionRpcResponse::Insert => Ok(()), + PartitionRpcResponse::Err(err) => Err(m_error!(EC::InternalErr, err)), + _ => Err(m_error!(EC::InternalErr, "unexpected insert rpc response")), + } + } + + async fn remote_delete( + &self, + target_worker_id: OID, + table_id: OID, + partition_id: OID, + key: Vec, + ) -> RS { + match self.send_partition_rpc( + target_worker_id, + PartitionRpcRequest::Delete { + table_id, + partition_id, + key, + }, + )? { + PartitionRpcResponse::Delete(rows) => Ok(rows), + PartitionRpcResponse::Err(err) => Err(m_error!(EC::InternalErr, err)), + _ => Err(m_error!(EC::InternalErr, "unexpected delete rpc response")), + } + } + + async fn remote_update( + &self, + target_worker_id: OID, + table_id: OID, + partition_id: OID, + key: Vec, + values: Vec<(AttrIndex, Vec)>, + ) -> RS { + match self.send_partition_rpc( + target_worker_id, + PartitionRpcRequest::Update { + table_id, + partition_id, + key, + values, + }, + )? { + PartitionRpcResponse::Update(rows) => Ok(rows), + PartitionRpcResponse::Err(err) => Err(m_error!(EC::InternalErr, err)), + _ => Err(m_error!(EC::InternalErr, "unexpected update rpc response")), + } + } + fn _begin_tx(&self) -> Arc { Arc::new(WorkerTxManager::new(self.snapshot_mgr.begin_tx())) } async fn _insert( - & self, + &self, desc: Arc, tx_mgr: Arc, table_id: OID, @@ -391,16 +737,35 @@ impl IoUringXContract { ) -> RS<()> { let key = build_key_tuple(keys, &desc)?; let value = build_value_tuple(values, &desc)?; - let contain_key = self.storage.get(table_id, &key, tx_mgr.as_ref()).await?; + let target_partition = self + .partition_router + .route_exact_partition(table_id, desc.as_ref(), keys) + .await?; + if let Some(partition_id) = target_partition { + match self.resolve_partition_worker(partition_id).await? { + Some(worker_id) if self.worker_id != 0 && worker_id != self.worker_id => { + return self + .remote_insert(worker_id, table_id, partition_id, key, value) + .await; + } + _ => {} + } + } + let contain_key = self + .storage + .get_on_partition(table_id, target_partition, &key, tx_mgr.as_ref()) + .await?; if contain_key.is_some() { Err(m_error!(EC::ExistingSuchElement, "existing key")) } else { - self.storage.put(table_id, key, value, tx_mgr.as_ref()).await + self.storage + .put_on_partition(table_id, target_partition, key, value, tx_mgr.as_ref()) + .await } } async fn _read_key( - & self, + &self, desc: Arc, tx_mgr: Arc, table_id: OID, @@ -409,15 +774,44 @@ impl IoUringXContract { _opt_read: &OptRead, ) -> RS>> { let key = build_key_tuple(pred_key, &desc)?; - let opt_value = self.storage.get(table_id, &key, tx_mgr.as_ref()).await?; + let target_partition = self + .partition_router + .route_exact_partition(table_id, desc.as_ref(), pred_key) + .await?; + let opt_value = match target_partition { + Some(partition_id) => match self.resolve_partition_worker(partition_id).await? { + Some(worker_id) if self.worker_id != 0 && worker_id != self.worker_id => { + self.remote_read_key( + worker_id, + table_id, + partition_id, + key.clone(), + select.vec().to_vec(), + ) + .await? + } + _ => self + .storage + .get_on_partition(table_id, Some(partition_id), &key, tx_mgr.as_ref()) + .await? + .map(|value| project_selected_fields(&desc, &key, &value, select)) + .transpose()?, + }, + None => self + .storage + .get_on_partition(table_id, None, &key, tx_mgr.as_ref()) + .await? + .map(|value| project_selected_fields(&desc, &key, &value, select)) + .transpose()?, + }; match opt_value { - Some(value) => project_selected_fields(&desc, &key, &value, select).map(Some), + Some(value) => Ok(Some(value)), None => Ok(None), } } async fn _read_range( - & self, + &self, desc: Arc, tx_mgr: Arc, table_id: OID, @@ -429,13 +823,61 @@ impl IoUringXContract { ensure_supported_predicate(pred_non_key)?; let start = build_bound_key(pred_key.start(), &desc)?; let end = build_bound_key(pred_key.end(), &desc)?; - let rows = self.storage.range(table_id, (start, end), tx_mgr.as_ref()).await?; - let projected = rows - .into_iter() - .map(|(key, value)| { - project_selected_fields(&desc, &key, &value, select).map(TupleRow::new) - }) - .collect::>>()?; + let target_partitions = self + .partition_router + .route_range_partitions(table_id, desc.as_ref(), pred_key.start(), pred_key.end()) + .await?; + let mut projected = Vec::new(); + match target_partitions { + Some(partitions) => { + for partition_id in partitions { + match self.resolve_partition_worker(partition_id).await? { + Some(worker_id) if self.worker_id != 0 && worker_id != self.worker_id => { + let rows = self + .remote_read_range( + worker_id, + table_id, + partition_id, + rpc_bound_from_key_bound(pred_key.start(), &desc)?, + rpc_bound_from_key_bound(pred_key.end(), &desc)?, + select.vec().to_vec(), + ) + .await?; + for row in rows { + projected.push(TupleRow::new(row)); + } + } + _ => { + let rows = self + .storage + .range_on_partition( + table_id, + Some(partition_id), + (start.clone(), end.clone()), + tx_mgr.as_ref(), + ) + .await?; + for (key, value) in rows { + projected.push(TupleRow::new(project_selected_fields( + &desc, &key, &value, select, + )?)); + } + } + } + } + } + None => { + let rows = self + .storage + .range(table_id, (start, end), tx_mgr.as_ref()) + .await?; + for (key, value) in rows { + projected.push(TupleRow::new(project_selected_fields( + &desc, &key, &value, select, + )?)); + } + } + } Ok(Arc::new(VecCursor { inner: Mutex::new(VecCursorInner { rows: projected, @@ -445,7 +887,7 @@ impl IoUringXContract { } async fn _delete( - & self, + &self, desc: Arc, tx_mgr: Arc, table_id: OID, @@ -455,12 +897,29 @@ impl IoUringXContract { ) -> RS { ensure_supported_predicate(pred_non_key)?; let key = build_key_tuple(pred_key, &desc)?; - let deleted = self.storage.remove(table_id, &key, tx_mgr.as_ref()).await?; + let target_partition = self + .partition_router + .route_exact_partition(table_id, desc.as_ref(), pred_key) + .await?; + if let Some(partition_id) = target_partition { + match self.resolve_partition_worker(partition_id).await? { + Some(worker_id) if self.worker_id != 0 && worker_id != self.worker_id => { + return self + .remote_delete(worker_id, table_id, partition_id, key) + .await; + } + _ => {} + } + } + let deleted = self + .storage + .remove_on_partition(table_id, target_partition, &key, tx_mgr.as_ref()) + .await?; Ok(usize::from(deleted.is_some())) } async fn _update( - & self, + &self, desc: Arc, tx_mgr: Arc, table_id: OID, @@ -471,13 +930,36 @@ impl IoUringXContract { ) -> RS { ensure_supported_predicate(pred_non_key)?; let key = build_key_tuple(pred_key, &desc)?; - let current = self.storage.get(table_id, &key, tx_mgr.as_ref()).await?; + let target_partition = self + .partition_router + .route_exact_partition(table_id, desc.as_ref(), pred_key) + .await?; + if let Some(partition_id) = target_partition { + match self.resolve_partition_worker(partition_id).await? { + Some(worker_id) if self.worker_id != 0 && worker_id != self.worker_id => { + return self + .remote_update( + worker_id, + table_id, + partition_id, + key, + values.data().clone(), + ) + .await; + } + _ => {} + } + } + let current = self + .storage + .get_on_partition(table_id, target_partition, &key, tx_mgr.as_ref()) + .await?; let Some(current) = current else { return Ok(0); }; let updated = apply_value_update(¤t, values, &desc)?; self.storage - .put(table_id, key, updated, tx_mgr.as_ref()) + .put_on_partition(table_id, target_partition, key, updated, tx_mgr.as_ref()) .await .map(|()| 1) } @@ -664,16 +1146,51 @@ fn build_tuple_for( if !ok { return Err(m_error!(EC::TupleErr)); } - let values: Vec<_> = vec_data.into_iter().map(|(_, v)| v).collect(); let tuple_desc = if IS_KEY { desc.key_desc() } else { desc.value_desc() }; - if tuple_desc.field_count() != values.len() { + let values: Vec<_> = vec_data.into_iter().map(|(_, v)| v).collect(); + if IS_KEY && tuple_desc.field_count() != values.len() { return Err(m_error!(EC::TupleErr)); } - build_tuple(&values, tuple_desc) + if IS_KEY { + return build_tuple(&values, tuple_desc); + } + + let value_len = tuple_desc.field_count(); + let mut completed = vec![None; value_len]; + for (attr, value) in data { + let field = desc.get_attr(*attr); + if field.primary_index().is_some() { + return Err(m_error!(EC::TupleErr)); + } + let datum_index = field.datum_index() as usize; + if datum_index >= value_len || completed[datum_index].is_some() { + return Err(m_error!(EC::TupleErr)); + } + completed[datum_index] = Some(value.clone()); + } + for attr in desc.value_indices() { + let field = desc.get_attr(*attr); + let datum_index = field.datum_index() as usize; + if completed[datum_index].is_some() { + continue; + } + let default = field + .type_desc() + .dat_type_id() + .fn_default()(field.type_desc()) + .map_err(|e| e.to_m_err())?; + completed[datum_index] = + Some(send_binary(&default, field.type_desc()).map_err(|e| e.to_m_err())?); + } + let completed = completed + .into_iter() + .collect::>>() + .ok_or_else(|| m_error!(EC::TupleErr))?; + build_tuple(&completed, tuple_desc) } fn build_bound_key( @@ -693,6 +1210,31 @@ fn build_bound_key( } } +fn rpc_bound_from_key_bound( + bound: &Bound>, + desc: &TableDesc, +) -> RS { + match bound { + Bound::Included(values) => Ok(RpcBound::Included(build_key_tuple( + &VecDatum::new(values.clone()), + desc, + )?)), + Bound::Excluded(values) => Ok(RpcBound::Excluded(build_key_tuple( + &VecDatum::new(values.clone()), + desc, + )?)), + Bound::Unbounded => Ok(RpcBound::Unbounded), + } +} + +fn rpc_bound_as_ref(bound: &RpcBound) -> Bound<&[u8]> { + match bound { + RpcBound::Included(bytes) => Bound::Included(bytes.as_slice()), + RpcBound::Excluded(bytes) => Bound::Excluded(bytes.as_slice()), + RpcBound::Unbounded => Bound::Unbounded, + } +} + fn project_selected_fields( desc: &TableDesc, key: &[u8], @@ -748,6 +1290,7 @@ fn single_put_batch(xid: u64, key: Vec, value: Vec) -> XLBatch { crate::wal::xl_entry::TxOp::Begin, crate::wal::xl_entry::TxOp::Insert(crate::wal::xl_data_op::XLInsert { table_id: 0, + partition_id: 0, tuple_id: 0, key, value, @@ -766,6 +1309,7 @@ fn single_delete_batch(xid: u64, key: Vec) -> XLBatch { crate::wal::xl_entry::TxOp::Begin, crate::wal::xl_entry::TxOp::Delete(crate::wal::xl_data_op::XLDelete { table_id: 0, + partition_id: 0, tuple_id: 0, key, }), @@ -786,6 +1330,7 @@ mod tests { use futures::executor::block_on; use mudu::common::id::gen_oid; use mudu_type::dat_type_id::DatTypeID; + use mudu_type::dt_fn_param::DatType; use mudu_type::dt_info::DTInfo; use std::collections::HashMap; use std::env::temp_dir; @@ -867,6 +1412,62 @@ mod tests { VecDatum::new(vec![(1, datum(v))]) } + fn datum_string(v: &str) -> Vec { + mudu_type::dt_function::send_binary( + &mudu_type::dat_value::DatValue::from_string(v.to_string()), + &mudu_type::dat_type::DatType::default_for(mudu_type::dat_type_id::DatTypeID::String), + ) + .unwrap() + } + + fn wallet_users_schema() -> SchemaTable { + use crate::contract::schema_column::SchemaColumn; + use mudu_type::dt_info::DTInfo; + + SchemaTable::new( + "users".to_string(), + vec![ + SchemaColumn::new( + "user_id".to_string(), + DatTypeID::I32, + DTInfo::from_opt_object(&DatType::default_for(DatTypeID::I32)), + ), + SchemaColumn::new( + "name".to_string(), + DatTypeID::String, + DTInfo::from_opt_object(&DatType::default_for(DatTypeID::String)), + ), + SchemaColumn::new( + "phone".to_string(), + DatTypeID::String, + DTInfo::from_opt_object(&DatType::default_for(DatTypeID::String)), + ), + SchemaColumn::new( + "email".to_string(), + DatTypeID::String, + DTInfo::from_opt_object(&DatType::default_for(DatTypeID::String)), + ), + SchemaColumn::new( + "password".to_string(), + DatTypeID::String, + DTInfo::from_opt_object(&DatType::default_for(DatTypeID::String)), + ), + SchemaColumn::new( + "created_at".to_string(), + DatTypeID::I32, + DTInfo::from_opt_object(&DatType::default_for(DatTypeID::I32)), + ), + SchemaColumn::new( + "updated_at".to_string(), + DatTypeID::I32, + DTInfo::from_opt_object(&DatType::default_for(DatTypeID::I32)), + ), + ], + vec![0], + vec![1, 2, 3, 4, 5, 6], + ) + } + #[test] fn relation_commit_log_round_trips() { let mgr = Arc::new(TestMetaMgr::new()); @@ -961,12 +1562,14 @@ mod tests { TxOp::Begin, TxOp::Insert(XLInsert { table_id: 0, + partition_id: 0, tuple_id: 0, key: b"wk".to_vec(), value: b"wv".to_vec(), }), TxOp::Insert(XLInsert { table_id, + partition_id: 0, tuple_id: 0, key: build_key_tuple(&key_row(3), &meta_table(&schema).unwrap()).unwrap(), value: build_value_tuple(&value_row(30), &meta_table(&schema).unwrap()) @@ -993,6 +1596,21 @@ mod tests { assert_eq!(relation, Some(vec![datum(30)])); } + #[test] + fn build_value_tuple_supports_partial_insert_with_mixed_types() { + let schema = wallet_users_schema(); + let desc = meta_table(&schema).unwrap(); + let input = VecDatum::new(vec![ + (1, datum_string("Alice")), + (2, datum_string("12345678")), + (3, datum_string("alice@xxx.com")), + (4, datum_string("aaa")), + (5, datum(0)), + ]); + let tuple = build_value_tuple(&input, &desc).unwrap(); + assert!(!tuple.is_empty()); + } + #[test] fn iouring_xcontract_replay_applies_worker_kv_delete() { let contract = IoUringXContract::with_worker_log( @@ -1015,6 +1633,7 @@ mod tests { TxOp::Begin, TxOp::Delete(crate::wal::xl_data_op::XLDelete { table_id: 0, + partition_id: 0, tuple_id: 0, key: b"wk".to_vec(), }), diff --git a/mudu_kernel/src/server/x_lock_mgr.rs b/mudu_kernel/src/server/x_lock_mgr.rs index b7d2997..efe480a 100644 --- a/mudu_kernel/src/server/x_lock_mgr.rs +++ b/mudu_kernel/src/server/x_lock_mgr.rs @@ -1,9 +1,10 @@ use mudu::common::id::OID; +use crate::x_engine::tx_mgr::PhysicalRelationId; use std::collections::HashMap; use std::sync::Mutex; pub struct XLockMgr { - lock: Mutex, OID>>>, + lock: Mutex, OID>>>, } impl XLockMgr { @@ -13,10 +14,10 @@ impl XLockMgr { } } - pub fn try_lock_some(&self, oid: OID, table_keys: &Vec<(OID, Vec)>) -> bool { + pub fn try_lock_some(&self, oid: OID, table_keys: &Vec<(PhysicalRelationId, Vec)>) -> bool { let mut lock = self.lock.lock().unwrap(); - for (table_oid, key) in table_keys.iter() { - let map = lock.entry(table_oid.clone()).or_default(); + for (relation_id, key) in table_keys.iter() { + let map = lock.entry(*relation_id).or_default(); if map.contains_key(key) { return false; } else { @@ -26,10 +27,10 @@ impl XLockMgr { true } - pub fn release(&self, oid: OID, table_keys: &Vec<(OID, Vec)>) { + pub fn release(&self, oid: OID, table_keys: &Vec<(PhysicalRelationId, Vec)>) { let mut lock = self.lock.lock().unwrap(); - for (table_oid, key) in table_keys.iter() { - let map = lock.entry(table_oid.clone()).or_default(); + for (relation_id, key) in table_keys.iter() { + let map = lock.entry(*relation_id).or_default(); if let Some(tx) = map.get(key) { if *tx == oid { map.remove(key); diff --git a/mudu_kernel/src/sql/binder.rs b/mudu_kernel/src/sql/binder.rs index 94b8242..e275a6f 100644 --- a/mudu_kernel/src/sql/binder.rs +++ b/mudu_kernel/src/sql/binder.rs @@ -1,11 +1,16 @@ use crate::contract::meta_mgr::MetaMgr; +use crate::contract::partition_rule::{ + PartitionBound, PartitionRuleDesc, RangePartitionDef, +}; +use crate::contract::partition_rule_binding::{PartitionPlacement, TablePartitionBinding}; use crate::contract::schema_column::SchemaColumn; use crate::contract::schema_table::SchemaTable; use crate::contract::table_desc::TableDesc; use crate::executor::project_tuple_desc; use crate::sql::bound_stmt::{ - BoundCommand, BoundCopyFrom, BoundCopyTo, BoundCreateTable, BoundDelete, BoundDropTable, - BoundInsert, BoundPredicate, BoundQuery, BoundSelect, BoundStmt, BoundUpdate, + BoundCommand, BoundCopyFrom, BoundCopyTo, BoundCreatePartitionPlacement, + BoundCreatePartitionRule, BoundCreateTable, BoundDelete, BoundDropTable, BoundInsert, + BoundPredicate, BoundQuery, BoundSelect, BoundStmt, BoundUpdate, }; use crate::sql::copy_layout::CopyLayout; use crate::sql::value_codec::ValueCodec; @@ -17,6 +22,10 @@ use mudu_type::dt_info::DTInfo; use sql_parser::ast::expr_compare::ExprCompare; use sql_parser::ast::expr_item::{ExprItem, ExprValue}; use sql_parser::ast::expr_operator::ValueCompare; +use sql_parser::ast::stmt_create_partition_placement::StmtCreatePartitionPlacement; +use sql_parser::ast::stmt_create_partition_rule::{ + StmtCreatePartitionRule, StmtPartitionBound, +}; use sql_parser::ast::stmt_create_table::StmtCreateTable; use sql_parser::ast::stmt_delete::StmtDelete; use sql_parser::ast::stmt_drop_table::StmtDropTable; @@ -48,6 +57,14 @@ impl Binder { async fn bind_command(&self, command: StmtCommand, params: &dyn SQLParams) -> RS { match command { + StmtCommand::CreatePartitionPlacement(stmt) => Ok( + BoundCommand::CreatePartitionPlacement( + self.bind_create_partition_placement(stmt).await?, + ), + ), + StmtCommand::CreatePartitionRule(stmt) => Ok(BoundCommand::CreatePartitionRule( + self.bind_create_partition_rule(stmt)?, + )), StmtCommand::CreateTable(stmt) => { Ok(BoundCommand::CreateTable(self.bind_create_table(stmt)?)) } @@ -110,16 +127,125 @@ impl Binder { .map(|index| index + value_offset) .collect(); columns.append(&mut value_columns); + let schema = SchemaTable::new(stmt.table_name().clone(), columns, key_indices, value_indices); + let partition_binding = if let Some(partition) = stmt.partition() { + let rule = futures::executor::block_on( + self.meta_mgr.get_partition_rule_by_name(partition.rule_name()), + )? + .ok_or_else(|| { + m_error!( + ER::NoSuchElement, + format!("no such partition rule {}", partition.rule_name()) + ) + })?; + let ref_attr_indices = partition + .reference_columns() + .iter() + .map(|column| { + schema + .columns() + .iter() + .position(|field| field.get_name() == column) + .ok_or_else(|| { + m_error!( + ER::NoSuchElement, + format!("no such partition reference column {}", column) + ) + }) + }) + .collect::>>()?; + if rule.partitions.is_empty() { + return Err(m_error!( + ER::ParseErr, + format!("partition rule {} has no partitions", partition.rule_name()) + )); + } + Some(TablePartitionBinding { + table_id: schema.id(), + rule_id: rule.oid, + ref_attr_indices, + }) + } else { + None + }; Ok(BoundCreateTable { - schema: SchemaTable::new( - stmt.table_name().clone(), - columns, - key_indices, - value_indices, - ), + schema, + partition_binding, + }) + } + + fn bind_create_partition_rule( + &self, + stmt: StmtCreatePartitionRule, + ) -> RS { + let partitions = stmt + .partitions() + .iter() + .map(|partition| { + Ok(RangePartitionDef::new( + partition.name().to_string(), + Self::bind_partition_bound(partition.start()), + Self::bind_partition_bound(partition.end()), + )) + }) + .collect::>>()?; + Ok(BoundCreatePartitionRule { + rule: PartitionRuleDesc::new_range(stmt.rule_name().to_string(), Vec::new(), partitions), }) } + async fn bind_create_partition_placement( + &self, + stmt: StmtCreatePartitionPlacement, + ) -> RS { + let rule = self + .meta_mgr + .get_partition_rule_by_name(stmt.rule_name()) + .await? + .ok_or_else(|| { + m_error!( + ER::NoSuchElement, + format!("no such partition rule {}", stmt.rule_name()) + ) + })?; + let mut placements = Vec::with_capacity(stmt.placements().len()); + for placement in stmt.placements() { + let partition = rule + .partitions + .iter() + .find(|partition| partition.name == placement.partition_name()) + .ok_or_else(|| { + m_error!( + ER::NoSuchElement, + format!( + "no such partition {} in rule {}", + placement.partition_name(), + stmt.rule_name() + ) + ) + })?; + let worker_id = placement.worker_id().parse::().map_err(|e| { + m_error!( + ER::ParseErr, + format!("invalid worker id {}", placement.worker_id()), + e + ) + })?; + placements.push(PartitionPlacement { + partition_id: partition.partition_id, + worker_id, + }); + } + Ok(BoundCreatePartitionPlacement { placements }) + } + + fn bind_partition_bound(bound: &StmtPartitionBound) -> PartitionBound { + match bound { + StmtPartitionBound::Unbounded => PartitionBound::Unbounded, + StmtPartitionBound::Value(values) => PartitionBound::Value(values.clone()), + } + } + async fn bind_drop_table(&self, stmt: StmtDropTable) -> RS { match self .meta_mgr diff --git a/mudu_kernel/src/sql/bound_stmt.rs b/mudu_kernel/src/sql/bound_stmt.rs index a75d2d2..cb39dd3 100644 --- a/mudu_kernel/src/sql/bound_stmt.rs +++ b/mudu_kernel/src/sql/bound_stmt.rs @@ -1,3 +1,5 @@ +use crate::contract::partition_rule::PartitionRuleDesc; +use crate::contract::partition_rule_binding::{PartitionPlacement, TablePartitionBinding}; use crate::contract::schema_table::SchemaTable; use mudu::common::id::{AttrIndex, OID}; use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; @@ -16,6 +18,8 @@ pub enum BoundQuery { #[derive(Clone, Debug)] pub enum BoundCommand { + CreatePartitionPlacement(BoundCreatePartitionPlacement), + CreatePartitionRule(BoundCreatePartitionRule), CreateTable(BoundCreateTable), DropTable(BoundDropTable), Insert(BoundInsert), @@ -33,9 +37,20 @@ pub struct BoundSelect { pub predicate: BoundPredicate, } +#[derive(Clone, Debug)] +pub struct BoundCreatePartitionRule { + pub rule: PartitionRuleDesc, +} + +#[derive(Clone, Debug)] +pub struct BoundCreatePartitionPlacement { + pub placements: Vec, +} + #[derive(Clone, Debug)] pub struct BoundCreateTable { pub schema: SchemaTable, + pub partition_binding: Option, } #[derive(Clone, Debug)] diff --git a/mudu_kernel/src/sql/build_select.rs b/mudu_kernel/src/sql/build_select.rs deleted file mode 100644 index b50d6e3..0000000 --- a/mudu_kernel/src/sql/build_select.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::contract::table_desc::TableDesc; -use crate::sql::proj_field::ProjField; -use mudu::common::id::OID; -use mudu::common::result::RS; -use mudu::error::ec::EC as ER; -use mudu::m_error; -use mudu_type::dat_type::DatType; -use sql_parser::ast::select_term::SelectTerm; - -pub fn visit_select_term( - select_term: &Vec, - table_desc: &TableDesc, -) -> RS<(Vec, Vec, Vec)> { - let mut ids = vec![]; - let mut proj_fields = vec![]; - let mut type_desc_vec = vec![]; - for (i, term) in select_term.iter().enumerate() { - let oid = table_desc.name2oid().get(term.field().name()).map_or( - Err(m_error!( - ER::NoSuchElement, - format!("{}", term.field().name()) - )), - |id| Ok(*id), - )?; - ids.push(oid); - let name = if term.alias().is_empty() { - term.field().name().clone() - } else { - term.alias().clone() - }; - let f = table_desc - .oid2col() - .get(&oid) - .map_or(Err(m_error!(ER::NoSuchElement, format!("{}", oid))), Ok)?; - let type_desc = f.type_desc().clone(); - proj_fields.push(ProjField::new(i, oid, name, type_desc.clone())); - type_desc_vec.push(type_desc); - } - Ok((ids, proj_fields, type_desc_vec)) -} diff --git a/mudu_kernel/src/sql/build_where_predicate.rs b/mudu_kernel/src/sql/build_where_predicate.rs deleted file mode 100644 index 690e2b4..0000000 --- a/mudu_kernel/src/sql/build_where_predicate.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::contract::table_desc::TableDesc; -use mudu::common::buf::Buf as Datum; -use mudu::common::id::OID; -use mudu::common::result::RS; -use mudu::error::ec::EC; -use mudu::m_error; -use sql_parser::ast::expr_compare::ExprCompare; -use sql_parser::ast::expr_literal::ExprLiteral; -use sql_parser::ast::expr_name::ExprName; -use sql_parser::ast::expr_operator::ValueCompare; - -fn convert_expr_compare_equal( - _expr: &ExprName, - _expr_literal: &ExprLiteral, - _desc: &TableDesc, -) -> RS<(OID, Datum)> { - Err(m_error!( - EC::NotImplemented, - "equality predicate conversion is not implemented" - )) -} - -fn convert_expr_compare(expr: &ExprCompare, _desc: &TableDesc) -> RS<(OID, Datum)> { - match expr.op() { - ValueCompare::EQ => match (expr.left(), expr.right()) { - _ => Err(m_error!( - EC::NotImplemented, - "only simple equality predicates are supported" - )), - }, - _ => Err(m_error!( - EC::NotImplemented, - "non-equality predicates are not implemented" - )), - } -} - -pub fn convert_exprs(exprs: &Vec, table_desc: &TableDesc) -> RS> { - let mut vec = vec![]; - for expr in exprs.iter() { - let datum = convert_expr_compare(expr, table_desc)?; - vec.push(datum) - } - Ok(vec) -} diff --git a/mudu_kernel/src/sql/cmp_pred.rs b/mudu_kernel/src/sql/cmp_pred.rs deleted file mode 100644 index 006cc58..0000000 --- a/mudu_kernel/src/sql/cmp_pred.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[derive(Debug, Clone)] -pub struct CmpPred { - attr: String, - value: String, -} - -impl CmpPred { - pub fn new() -> Self { - Self { - attr: "".to_string(), - value: "".to_string(), - } - } -} diff --git a/mudu_kernel/src/sql/describer.rs b/mudu_kernel/src/sql/describer.rs index e81d9a3..8b8dfe1 100644 --- a/mudu_kernel/src/sql/describer.rs +++ b/mudu_kernel/src/sql/describer.rs @@ -8,16 +8,14 @@ use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; use sql_parser::ast::stmt_type::StmtType; use std::sync::Arc; -pub struct Describer { - -} +pub struct Describer {} impl Describer { pub fn new() -> Self { - Self { } + Self {} } - pub async fn describe(meta_mgr:&dyn MetaMgr, stmt: StmtType) -> RS { + pub async fn describe(meta_mgr: &dyn MetaMgr, stmt: StmtType) -> RS { match stmt { StmtType::Select(stmt) => Self::describe_select(meta_mgr, stmt).await, StmtType::Command(_) => Ok(TupleFieldDesc::new(Vec::new())), @@ -25,7 +23,7 @@ impl Describer { } async fn describe_select( - meta_mgr:&dyn MetaMgr, + meta_mgr: &dyn MetaMgr, stmt: sql_parser::ast::stmt_select::StmtSelect, ) -> RS { let table_desc = Self::get_table_by_name(meta_mgr, stmt.get_table_reference()).await?; @@ -53,7 +51,7 @@ impl Describer { .ok_or_else(|| m_error!(ER::NoSuchElement, format!("cannot find column {}", name))) } - async fn get_table_by_name(meta_mgr:&dyn MetaMgr, name: &String) -> RS> { + async fn get_table_by_name(meta_mgr: &dyn MetaMgr, name: &String) -> RS> { meta_mgr .get_table_by_name(name) .await? diff --git a/mudu_kernel/src/sql/mod.rs b/mudu_kernel/src/sql/mod.rs index d89379d..1ca6dc3 100644 --- a/mudu_kernel/src/sql/mod.rs +++ b/mudu_kernel/src/sql/mod.rs @@ -1,6 +1,5 @@ #![allow(dead_code)] -mod cmp_pred; mod copy_layout; #[cfg(test)] mod copy_layout_test; @@ -22,11 +21,7 @@ mod binder_test; pub mod stmt_cmd; mod stmt_create_table; -mod build_select; -mod build_where_predicate; - mod current_tx; -mod plan_param; mod stmt_copy_from; mod stmt_copy_to; diff --git a/mudu_kernel/src/sql/plan_param.rs b/mudu_kernel/src/sql/plan_param.rs deleted file mode 100644 index 518b563..0000000 --- a/mudu_kernel/src/sql/plan_param.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::any::Any; - -pub type PParam = Box; - -/// use Box::into_inner when it is stable -fn box_into_inner(boxed: Box) -> T { - *boxed -} - -pub fn downcast_param(param: PParam) -> T { - let r_downcast = param.downcast::(); - match r_downcast { - Ok(param) => box_into_inner(param), - Err(_e) => { - panic!("downcast to build parameter error") - } - } -} diff --git a/mudu_kernel/src/sql/planner.rs b/mudu_kernel/src/sql/planner.rs index 257ede3..1ca3178 100644 --- a/mudu_kernel/src/sql/planner.rs +++ b/mudu_kernel/src/sql/planner.rs @@ -1,3 +1,5 @@ +use crate::command::create_partition_placement::CreatePartitionPlacement; +use crate::command::create_partition_rule::CreatePartitionRule; use crate::command::create_table::CreateTable; use crate::command::delete_key_value::DeleteKeyValue; use crate::command::drop_table::DropTable; @@ -8,14 +10,15 @@ use crate::command::update_key_value::UpdateKeyValue; use crate::contract::cmd_exec::CmdExec; use crate::contract::query_exec::QueryExec; use crate::sql::bound_stmt::{ - BoundCommand, BoundCopyFrom, BoundCopyTo, BoundCreateTable, BoundDelete, BoundDropTable, - BoundInsert, BoundPredicate, BoundQuery, BoundSelect, BoundUpdate, + BoundCommand, BoundCopyFrom, BoundCopyTo, BoundCreatePartitionPlacement, + BoundCreatePartitionRule, BoundCreateTable, BoundDelete, BoundDropTable, BoundInsert, + BoundPredicate, BoundQuery, BoundSelect, BoundUpdate, }; use crate::sql::plan_ctx::PlanCtx; use crate::x_engine::api::{OptRead, Predicate, RangeData, VecDatum, VecSelTerm}; use crate::x_engine::x_param::{ - PAccessKey, PAccessRange, PCreateTable, PDeleteKeyValue, PDropTable, PInsertKeyValue, - PUpdateKeyValue, + PAccessKey, PAccessRange, PCreatePartitionPlacement, PCreatePartitionRule, PCreateTable, + PDeleteKeyValue, PDropTable, PInsertKeyValue, PUpdateKeyValue, }; use mudu::common::result::RS; use std::sync::Arc; @@ -37,6 +40,12 @@ impl Planner { pub async fn plan_command(&self, command: BoundCommand) -> RS> { match command { + BoundCommand::CreatePartitionPlacement(stmt) => { + Ok(Arc::new(self.plan_create_partition_placement(stmt))) + } + BoundCommand::CreatePartitionRule(stmt) => { + Ok(Arc::new(self.plan_create_partition_rule(stmt))) + } BoundCommand::CreateTable(stmt) => Ok(Arc::new(self.plan_create_table(stmt))), BoundCommand::DropTable(stmt) => Ok(Arc::new(self.plan_drop_table(stmt))), BoundCommand::Insert(stmt) => Ok(Arc::new(self.plan_insert(stmt))), @@ -103,11 +112,35 @@ impl Planner { } } + fn plan_create_partition_placement( + &self, + stmt: BoundCreatePartitionPlacement, + ) -> CreatePartitionPlacement { + CreatePartitionPlacement::new( + PCreatePartitionPlacement { + tx_mgr: self.ctx.tx_mgr.clone(), + placements: stmt.placements, + }, + self.ctx.meta_mgr.clone(), + ) + } + + fn plan_create_partition_rule(&self, stmt: BoundCreatePartitionRule) -> CreatePartitionRule { + CreatePartitionRule::new( + PCreatePartitionRule { + tx_mgr: self.ctx.tx_mgr.clone(), + rule: stmt.rule, + }, + self.ctx.meta_mgr.clone(), + ) + } + fn plan_create_table(&self, stmt: BoundCreateTable) -> CreateTable { CreateTable::new( PCreateTable { tx_mgr: self.ctx.tx_mgr.clone(), schema: stmt.schema, + partition_binding: stmt.partition_binding, }, self.ctx.x_contract.clone(), self.ctx.meta_mgr.clone(), diff --git a/mudu_kernel/src/storage/mem_store.rs b/mudu_kernel/src/storage/mem_store.rs deleted file mode 100644 index e856ea7..0000000 --- a/mudu_kernel/src/storage/mem_store.rs +++ /dev/null @@ -1,107 +0,0 @@ -use async_trait::async_trait; -use mudu::common::buf::Buf; -use mudu::common::id::OID; -use mudu::error::ec::EC as ER; -use scc::HashIndex; -use std::collections::Bound; -use std::sync::Arc; - -use crate::contract::data_row::DataRow; -use crate::contract::mem_store::MemStore; -use crate::storage::mem_table::MemTable; -use mudu::common::result::RS; -use mudu::m_error; -use mudu_contract::tuple::tuple_binary_desc::TupleBinaryDesc as TupleDesc; - -#[derive(Clone)] -pub struct MemStoreImpl { - hash: Arc>>, -} - -impl MemStoreImpl { - pub fn new() -> Self { - Self { - hash: Arc::new(HashIndex::new()), - } - } - - fn _create_table(&self, oid: OID, key_desc: TupleDesc) -> RS<()> { - let table = MemTable::new(key_desc); - let r = self.hash.insert_sync(oid, Arc::new(table)); - r.map_err(|(_, _)| m_error!(ER::ExistingSuchElement))?; - Ok(()) - } - - fn _drop_table(&self, oid: OID) -> RS<()> { - let r = self.hash.remove_sync(&oid); - if !r { - return Err(m_error!(ER::NoSuchElement)); - } - Ok(()) - } - - fn _get_key>(&self, oid: OID, key: K) -> RS> { - let opt = self.hash.get_sync(&oid); - match opt { - Some(e) => e.get().read_key(key), - None => Err(m_error!( - ER::NoSuchElement, - format!("table id {} not found in store", oid) - )), - } - } - - fn _read_range>( - &self, - oid: OID, - begin: Bound, - end: Bound, - ) -> RS> { - let opt = self.hash.get_sync(&oid); - match opt { - Some(e) => e.get().read_range(begin, end), - None => Err(m_error!( - ER::NoSuchElement, - format!("table id {} not found in store", oid) - )), - } - } - - fn _insert_key(&self, oid: OID, key: Buf, row: DataRow) -> RS> { - let opt = self.hash.get_sync(&oid); - match opt { - Some(e) => e.get().insert_key(key, row), - None => Err(m_error!( - ER::NoSuchElement, - format!("table id {} not found in store", oid) - )), - } - } -} - -#[async_trait] -impl MemStore for MemStoreImpl { - async fn create_table(&self, oid: OID, key_desc: TupleDesc) -> RS<()> { - self._create_table(oid, key_desc) - } - - async fn drop_table(&self, oid: OID) -> RS<()> { - self._drop_table(oid) - } - - async fn get_key(&self, oid: OID, key: Buf) -> RS> { - self._get_key(oid, key) - } - - async fn read_range(&self, oid: OID, begin: Bound, end: Bound) -> RS> { - self._read_range(oid, begin, end) - } - - async fn insert_key(&self, oid: OID, key: Buf, row: DataRow) -> RS> { - self._insert_key(oid, key, row) - } -} - -unsafe impl Send for MemStoreImpl {} - -unsafe impl Sync for MemStoreImpl {} diff --git a/mudu_kernel/src/storage/mem_store_factory.rs b/mudu_kernel/src/storage/mem_store_factory.rs deleted file mode 100644 index 5d10af8..0000000 --- a/mudu_kernel/src/storage/mem_store_factory.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::contract::mem_store::MemStore; -use crate::storage::mem_store::MemStoreImpl; -use mudu::common::result::RS; -use std::sync::Arc; - -pub struct MemStoreFactory {} - -impl MemStoreFactory { - pub fn create(_path: String) -> RS> { - Ok(Arc::new(MemStoreImpl::new())) - } -} diff --git a/mudu_kernel/src/storage/mem_table.rs b/mudu_kernel/src/storage/mem_table.rs deleted file mode 100644 index 1a7bcd8..0000000 --- a/mudu_kernel/src/storage/mem_table.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::contract::data_row::DataRow; -use mudu::common::buf::Buf; -use mudu::common::result::RS; -use mudu_contract::tuple::tuple_binary_desc::TupleBinaryDesc as TupleDesc; -use mudu_contract::tuple::tuple_key::{_KeyRef, TupleKey}; -use scc::TreeIndex; -use std::collections::Bound; -use std::sync::Arc; - -pub struct MemTable { - inner: Arc, -} - -struct MemTableI { - key_desc: TupleDesc, - tree_index: TreeIndex, -} - -impl MemTable { - pub fn new(key_desc: TupleDesc) -> Self { - Self { - inner: Arc::new(MemTableI::new(key_desc)), - } - } - - pub fn read_key>(&self, key: K) -> RS> { - self.inner.read_key(key) - } - - pub fn read_range>(&self, begin: Bound, end: Bound) -> RS> { - self.inner.read_range(begin, end) - } - - pub fn insert_key(&self, key: Buf, row: DataRow) -> RS> { - self.inner.insert_key(key, row) - } -} -impl MemTableI { - pub fn new(key_desc: TupleDesc) -> Self { - Self { - key_desc, - tree_index: TreeIndex::new(), - } - } - - pub fn read_key>(&self, key: K) -> RS> { - let _key_ref = _KeyRef::new(&key); - todo!(); - /* - let opt_r = self.tree_index.peek(todo!(), &g); - let r = opt_r.cloned(); - Ok(r) - */ - } - - pub fn read_range>(&self, _begin: Bound, _end: Bound) -> RS> { - todo!() - /* - let mut rows = vec![]; - let g = Guard::new(); - let begin_bound = begin.map(|k| _Key::new(k)); - let end_bound = end.map(|k| _Key::new(k)); - - let mut range = self.tree_index.range((begin_bound, end_bound), &g); - loop { - let opt = range.next(); - match opt { - Some((_k, v)) => rows.push(v.clone()), - None => { - break; - } - } - } - Ok(rows) - */ - } - - pub fn insert_key(&self, key: Buf, row: DataRow) -> RS> { - let key = TupleKey::from_buf(&self.key_desc, key); - let r = self.tree_index.insert_sync(key, row); - match r { - Ok(()) => Ok(None), - Err((k, v)) => Ok(Some((k.into(), v))), - } - } -} diff --git a/mudu_kernel/src/storage/mod.rs b/mudu_kernel/src/storage/mod.rs index 687708b..64666fb 100644 --- a/mudu_kernel/src/storage/mod.rs +++ b/mudu_kernel/src/storage/mod.rs @@ -1,13 +1,3 @@ -mod mem_store; -pub mod mem_store_factory; -mod mem_table; pub mod page; - -pub mod pst_op_ch; -mod pst_op_ch_impl; - -pub mod pst_store_factory; -mod pst_store_impl; pub mod relation; -mod test_pst_store; pub mod time_series; diff --git a/mudu_kernel/src/storage/pst_op_ch.rs b/mudu_kernel/src/storage/pst_op_ch.rs deleted file mode 100644 index 7ff08e2..0000000 --- a/mudu_kernel/src/storage/pst_op_ch.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::contract::pst_op_list::PstOpList; -use mudu::common::result::RS; - -pub trait PstOpCh: Send + Sync { - fn async_run(&self, ops: PstOpList) -> RS<()>; -} diff --git a/mudu_kernel/src/storage/pst_op_ch_impl.rs b/mudu_kernel/src/storage/pst_op_ch_impl.rs deleted file mode 100644 index 2ba8dbe..0000000 --- a/mudu_kernel/src/storage/pst_op_ch_impl.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::contract::pst_op_list::PstOpList; -use crate::storage::pst_op_ch::PstOpCh; -use crate::storage::pst_store_impl::PstOpChImpl; -use mudu::common::result::RS; - -impl PstOpCh for PstOpChImpl { - fn async_run(&self, ops: PstOpList) -> RS<()> { - self.async_run_ops(ops.into_ops()) - } -} diff --git a/mudu_kernel/src/storage/pst_store_factory.rs b/mudu_kernel/src/storage/pst_store_factory.rs deleted file mode 100644 index f6bc9a2..0000000 --- a/mudu_kernel/src/storage/pst_store_factory.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::storage::pst_op_ch::PstOpCh; -use crate::storage::pst_store_impl::{PstOpChImpl, PstStoreImpl}; -use mudu::common::result::RS; -use mudu_utils::sync::s_task::SyncTask; -use mudu_utils::sync::unique_inner::UniqueInner; -use std::fs; -use std::path::PathBuf; -use std::sync::mpsc::channel; -use std::sync::Arc; - -pub struct PstStoreFactory {} - -impl PstStoreFactory { - pub fn create(db_path: String) -> RS<(SyncTask, Arc)> { - let mut path = PathBuf::from(db_path); - path.push("db"); - if !path.exists() { - fs::create_dir_all(&path).unwrap(); - } - path.push("kv.db"); - let path = path.to_str().unwrap().to_string(); - let (s, r) = channel(); - let store = PstStoreImpl::new(path, r)?; - let task = Arc::new(UniqueInner::new(store)); - let ch = PstOpChImpl::new(s); - Ok((task, Arc::new(ch))) - } -} diff --git a/mudu_kernel/src/storage/pst_store_impl.rs b/mudu_kernel/src/storage/pst_store_impl.rs deleted file mode 100644 index 298cdcf..0000000 --- a/mudu_kernel/src/storage/pst_store_impl.rs +++ /dev/null @@ -1,237 +0,0 @@ -use crate::contract::pst_op::{DeleteKV, InsertKV, PstOp, UpdateV}; -use mudu::common::id::OID; -use mudu::common::result::RS; -use mudu::error::ec::EC as ER; -use mudu::m_error; -use mudu_utils::sync::s_task::STask; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::mpsc::{Receiver, Sender}; -use std::sync::{Arc, Mutex}; - -pub struct PstStoreImpl { - name: String, - inner: Arc>, -} - -#[derive(Clone)] -pub struct PstOpChImpl { - sender: Sender>, -} - -impl PstOpChImpl { - pub fn new(sender: Sender>) -> PstOpChImpl { - Self { sender } - } - - pub fn async_run_ops(&self, ops: Vec) -> RS<()> { - self.sender - .send(ops) - .map_err(|e| m_error!(ER::IOErr, "", e))?; - Ok(()) - } -} -impl PstStoreImpl { - pub fn new(db_path: String, receiver: Receiver>) -> RS { - let s = Self { - name: "PST store flush".to_string(), - inner: Arc::new(Mutex::new(Inner::new(db_path, receiver)?)), - }; - Ok(s) - } - - pub fn run_flush(&self) -> RS<()> { - let mut guard = self - .inner - .lock() - .map_err(|_e| m_error!(ER::MutexError, ""))?; - guard.run_flush()?; - Ok(()) - } -} - -struct Inner { - receiver: Receiver>, - path: PathBuf, - state: PersistedStore, -} - -impl Inner { - fn new(path: String, receiver: Receiver>) -> RS { - let path = PathBuf::from(path); - let state = PersistedStore::load(&path)?; - Ok(Self { - receiver, - path, - state, - }) - } - - fn create(&mut self) -> RS<()> { - if let Some(parent) = self.path.parent() { - fs::create_dir_all(parent) - .map_err(|e| m_error!(ER::IOErr, "create pst store dir error", e))?; - } - Ok(()) - } - - fn run_flush(&mut self) -> RS<()> { - self.create()?; - let channel = &self.receiver; - loop { - let mut vec_cmds = channel.recv().map_err(|e| m_error!(ER::IOErr, "", e))?; - let try_iter = channel.try_iter(); - for c in try_iter { - vec_cmds.extend(c); - } - - let ok = Self::write(&self.path, &mut self.state, vec_cmds)?; - if !ok { - // stopped - break; - } - } - Ok(()) - } - - fn write(path: &Path, state: &mut PersistedStore, cmds: Vec) -> RS { - let mut notify = vec![]; - let mut stop = None; - for c in cmds { - match c { - PstOp::InsertKV(insert_kv) => { - Self::insert_kv(state, insert_kv); - } - PstOp::UpdateV(update_v) => { - Self::update_v(state, update_v)?; - } - PstOp::DeleteKV(delete_kv) => { - Self::delete_kv(state, delete_kv); - } - PstOp::WriteDelta(_) => {} - PstOp::Flush(n) => { - notify.push(n); - } - PstOp::Stop(n) => { - stop = Some(n); - break; - } - } - } - state.save(path)?; - for n in notify { - let _ = n.send(()); - } - match stop { - None => {} - Some(notify) => { - let _ = notify.send(()); - return Ok(false); - } - } - Ok(true) - } - - fn insert_kv(state: &mut PersistedStore, insert_kv: InsertKV) { - let row_key = PersistedKey::new(insert_kv.table_id, insert_kv.tuple_id); - let row = PersistedRow { - table_id: oid_2_text(insert_kv.table_id), - tuple_id: oid_2_text(insert_kv.tuple_id), - ts_min: insert_kv.timestamp.c_min(), - ts_max: insert_kv.timestamp.c_max(), - tuple_key: insert_kv.key, - tuple_value: insert_kv.value, - }; - state.data.insert(row_key.as_map_key(), row); - } - - fn update_v(state: &mut PersistedStore, update_v: UpdateV) -> RS<()> { - let row_key = PersistedKey::new(update_v.table_id, update_v.tuple_id).as_map_key(); - let row = state - .data - .get_mut(&row_key) - .ok_or_else(|| m_error!(ER::IOErr, "update missing pst row"))?; - row.ts_min = update_v.timestamp.c_min(); - row.ts_max = update_v.timestamp.c_max(); - row.tuple_value = update_v.value; - Ok(()) - } - - fn delete_kv(state: &mut PersistedStore, delete_kv: DeleteKV) { - let row_key = PersistedKey::new(delete_kv.table_id, delete_kv.tuple_id).as_map_key(); - state.data.remove(&row_key); - } -} - -fn oid_2_text(oid: OID) -> String { - oid.to_string() -} - -#[derive(Default, Serialize, Deserialize)] -struct PersistedStore { - data: BTreeMap, -} - -impl PersistedStore { - fn load(path: &Path) -> RS { - if !path.exists() { - return Ok(Self::default()); - } - let text = fs::read_to_string(path) - .map_err(|e| m_error!(ER::IOErr, "read pst store file error", e))?; - serde_json::from_str(&text) - .map_err(|e| m_error!(ER::ParseErr, "parse pst store file error", e)) - } - - fn save(&self, path: &Path) -> RS<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|e| m_error!(ER::IOErr, "create pst store dir error", e))?; - } - let text = serde_json::to_string_pretty(self) - .map_err(|e| m_error!(ER::ParseErr, "serialize pst store file error", e))?; - let tmp_path = path.with_extension("json.tmp"); - fs::write(&tmp_path, text) - .map_err(|e| m_error!(ER::IOErr, "write pst store temp file error", e))?; - fs::rename(&tmp_path, path) - .map_err(|e| m_error!(ER::IOErr, "replace pst store file error", e))?; - Ok(()) - } -} - -#[derive(Serialize, Deserialize)] -struct PersistedRow { - table_id: String, - tuple_id: String, - ts_min: u64, - ts_max: u64, - tuple_key: Vec, - tuple_value: Vec, -} - -struct PersistedKey { - table_id: OID, - tuple_id: OID, -} - -impl PersistedKey { - fn new(table_id: OID, tuple_id: OID) -> Self { - Self { table_id, tuple_id } - } - - fn as_map_key(&self) -> String { - format!("{}:{}", self.table_id, self.tuple_id) - } -} - -impl STask for PstStoreImpl { - fn name(&self) -> String { - self.name.clone() - } - - fn run(self) -> RS<()> { - self.run_flush() - } -} diff --git a/mudu_kernel/src/storage/relation/relation.rs b/mudu_kernel/src/storage/relation/relation.rs index 3143821..95960e4 100644 --- a/mudu_kernel/src/storage/relation/relation.rs +++ b/mudu_kernel/src/storage/relation/relation.rs @@ -1,6 +1,6 @@ -use std::ops::Bound; -use std::cell::{Cell, UnsafeCell}; use futures::executor::block_on; +use std::cell::{Cell, UnsafeCell}; +use std::ops::Bound; use mudu::common::id::{TupleID, OID}; use mudu::common::result::RS; @@ -58,11 +58,19 @@ impl Relation { Ok(self.inner.visible_meta_sync(key, snapshot)?.is_some()) } - pub async fn visible_value(&self, key: &KeyTuple, snapshot: &WorkerSnapshot) -> RS>> { + pub async fn visible_value( + &self, + key: &KeyTuple, + snapshot: &WorkerSnapshot, + ) -> RS>> { self.inner.visible_value(key, snapshot).await } - pub fn visible_value_sync(&self, key: &KeyTuple, snapshot: &WorkerSnapshot) -> RS>> { + pub fn visible_value_sync( + &self, + key: &KeyTuple, + snapshot: &WorkerSnapshot, + ) -> RS>> { self.inner.visible_value_sync(key, snapshot) } @@ -194,7 +202,8 @@ impl RelationInner { let _ = self.index_mut().insert(key_tuple, row)?; } - self.next_tuple_id.set(max_tuple_id.saturating_add(1).max(1)); + self.next_tuple_id + .set(max_tuple_id.saturating_add(1).max(1)); Ok(()) } @@ -226,7 +235,11 @@ impl RelationInner { block_on(self.visible_meta(key, snapshot)) } - async fn visible_value(&self, key: &KeyTuple, snapshot: &WorkerSnapshot) -> RS>> { + async fn visible_value( + &self, + key: &KeyTuple, + snapshot: &WorkerSnapshot, + ) -> RS>> { let Some((tuple_id, version)) = self.visible_meta(key, snapshot).await? else { return Ok(None); }; diff --git a/mudu_kernel/src/storage/test_pst_store.rs b/mudu_kernel/src/storage/test_pst_store.rs deleted file mode 100644 index bb2599c..0000000 --- a/mudu_kernel/src/storage/test_pst_store.rs +++ /dev/null @@ -1,68 +0,0 @@ -#[cfg(test)] -mod _test { - use std::thread; - - use crate::contract::pst_op_list::PstOpList; - use crate::contract::timestamp::Timestamp; - use crate::storage::pst_store_factory::PstStoreFactory; - use mudu::common::result::RS; - use mudu_utils::log::log_setup; - use tokio::sync::oneshot; - use tracing::{error, info}; - - #[test] - fn test_pst_store() { - log_setup("debug"); - _test_pst_store().unwrap(); - info!("test_pst_store test success"); - } - - fn _test_pst_store() -> RS<()> { - let db = format!("/tmp/test_pst_store_{}", mudu_sys::random::uuid_v4()); - let (task, ch) = PstStoreFactory::create(db).unwrap(); - let thd_task = thread::Builder::new().spawn(move || { - let r = task.run_once(); - match r { - Ok(_) => {} - Err(e) => { - error!("run flush task error {}", e); - panic!("{}", e); - } - } - }); - let post_task = thread::Builder::new().spawn(move || { - for i in 0..1000 { - let mut ops = PstOpList::new(); - ops.push_insert( - i, - i, - Timestamp::new(0, 1), - Default::default(), - Default::default(), - ); - ops.push_update(i, i, Timestamp::new(2, 3), Default::default()); - - ch.async_run(ops).unwrap(); - let mut ops = PstOpList::new(); - ops.push_delete(i, i); - ch.async_run(ops).unwrap(); - } - let (s, r) = oneshot::channel(); - let mut ops = PstOpList::new(); - ops.push_stop(s); - ch.async_run(ops).unwrap(); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - runtime.block_on(async move { - r.await.unwrap(); - info!("notified"); - }); - }); - - post_task.unwrap().join().unwrap(); - thd_task.unwrap().join().unwrap(); - Ok(()) - } -} diff --git a/mudu_kernel/src/wal/worker_wal_backend.rs b/mudu_kernel/src/wal/worker_wal_backend.rs index d1ffa2d..63fde58 100644 --- a/mudu_kernel/src/wal/worker_wal_backend.rs +++ b/mudu_kernel/src/wal/worker_wal_backend.rs @@ -1073,6 +1073,7 @@ mod tests { TxOp::Begin, TxOp::Insert(XLInsert { table_id: 0, + partition_id: 0, tuple_id: 0, key: b"k1".to_vec(), value: b"v1".to_vec(), @@ -1122,6 +1123,7 @@ mod tests { TxOp::Begin, TxOp::Insert(XLInsert { table_id: 0, + partition_id: 0, tuple_id: 0, key: b"k2".to_vec(), value: b"v2".to_vec(), @@ -1203,6 +1205,7 @@ mod tests { TxOp::Begin, TxOp::Insert(XLInsert { table_id: 0, + partition_id: 0, tuple_id: 0, key: b"k".to_vec(), value: vec![9u8; 512], diff --git a/mudu_kernel/src/wal/xl_batch_worker_log.rs b/mudu_kernel/src/wal/xl_batch_worker_log.rs index a114850..88fff95 100644 --- a/mudu_kernel/src/wal/xl_batch_worker_log.rs +++ b/mudu_kernel/src/wal/xl_batch_worker_log.rs @@ -131,6 +131,7 @@ mod tests { TxOp::Begin, TxOp::Insert(XLInsert { table_id: 7, + partition_id: 0, tuple_id: xid as u64 + 10, key: format!("key-{xid}").into_bytes(), value: vec![xid as u8; payload_size], diff --git a/mudu_kernel/src/wal/xl_data_op.rs b/mudu_kernel/src/wal/xl_data_op.rs index 2839fa7..5f73a5e 100644 --- a/mudu_kernel/src/wal/xl_data_op.rs +++ b/mudu_kernel/src/wal/xl_data_op.rs @@ -9,6 +9,10 @@ pub struct XLInsert { /// Recovery uses this to locate which table should receive the inserted /// tuple. pub table_id: OID, + /// Physical partition identifier for relation rows. + /// + /// `0` is reserved for worker-local KV WAL records. + pub partition_id: OID, /// Tuple identifier assigned to the inserted row version. /// /// This is the logical tuple id within the target table, not a physical @@ -30,6 +34,10 @@ pub struct XLInsert { pub struct XLDelete { /// Target table object identifier. pub table_id: OID, + /// Physical partition identifier for relation rows. + /// + /// `0` is reserved for worker-local KV WAL records. + pub partition_id: OID, /// Tuple identifier of the row version being deleted. pub tuple_id: u64, /// Key bytes of the tuple to delete. @@ -44,6 +52,8 @@ pub struct XLDelete { pub struct XLUpdate { /// Target table object identifier. pub table_id: OID, + /// Physical partition identifier for relation rows. + pub partition_id: OID, /// Tuple identifier of the row version being updated. pub tuple_id: u64, /// Key bytes of the tuple to update. diff --git a/mudu_kernel/src/x_engine/mod.rs b/mudu_kernel/src/x_engine/mod.rs index fd6457b..a9e7f7d 100644 --- a/mudu_kernel/src/x_engine/mod.rs +++ b/mudu_kernel/src/x_engine/mod.rs @@ -4,5 +4,5 @@ pub mod api; pub mod operator; mod dat_bin; -pub mod x_param; pub(crate) mod tx_mgr; +pub mod x_param; diff --git a/mudu_kernel/src/x_engine/tx_mgr.rs b/mudu_kernel/src/x_engine/tx_mgr.rs index b58107e..055ce5c 100644 --- a/mudu_kernel/src/x_engine/tx_mgr.rs +++ b/mudu_kernel/src/x_engine/tx_mgr.rs @@ -3,6 +3,12 @@ use crate::wal::xl_batch::XLBatch; use mudu::common::id::OID; use std::collections::BTreeMap; +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct PhysicalRelationId { + pub table_id: OID, + pub partition_id: OID, +} + pub trait TxMgr: Send + Sync { fn xid(&self) -> u64; @@ -14,20 +20,22 @@ pub trait TxMgr: Send + Sync { fn get(&self, key: &[u8]) -> Option>>; - fn put_relation(&self, oid: OID, key: Vec, value: Vec); + fn put_relation(&self, relation_id: PhysicalRelationId, key: Vec, value: Vec); - fn delete_relation(&self, oid: OID, key: Vec); + fn delete_relation(&self, relation_id: PhysicalRelationId, key: Vec); - fn get_relation(&self, oid: OID, key: &[u8]) -> Option>>; + fn get_relation(&self, relation_id: PhysicalRelationId, key: &[u8]) -> Option>>; fn staged_relation_items_in_range( &self, - oid: OID, + relation_id: PhysicalRelationId, start_key: &[u8], end_key: &[u8], ) -> Vec<(Vec, Option>)>; - fn staged_relation_ops(&self) -> BTreeMap, Option>>>; + fn staged_relation_ops( + &self, + ) -> BTreeMap, Option>>>; fn staged_items_in_range( &self, @@ -39,10 +47,9 @@ pub trait TxMgr: Send + Sync { fn is_empty(&self) -> bool; - fn write_ops(&self) -> Vec<(OID, Vec)>; + fn write_ops(&self) -> Vec<(PhysicalRelationId, Vec)>; fn build_write_ops(&self); fn xl_batch(&self) -> XLBatch; } - diff --git a/mudu_kernel/src/x_engine/x_param.rs b/mudu_kernel/src/x_engine/x_param.rs index aa9f8a7..2117224 100644 --- a/mudu_kernel/src/x_engine/x_param.rs +++ b/mudu_kernel/src/x_engine/x_param.rs @@ -1,3 +1,5 @@ +use crate::contract::partition_rule::PartitionRuleDesc; +use crate::contract::partition_rule_binding::{PartitionPlacement, TablePartitionBinding}; use crate::contract::schema_table::SchemaTable; use crate::x_engine::api::{OptRead, Predicate, RangeData, VecDatum, VecSelTerm}; use crate::x_engine::tx_mgr::TxMgr; @@ -22,10 +24,23 @@ pub struct PAccessRange { pub opt_read: OptRead, } +#[derive(Clone)] +pub struct PCreatePartitionRule { + pub tx_mgr: Arc, + pub rule: PartitionRuleDesc, +} + +#[derive(Clone)] +pub struct PCreatePartitionPlacement { + pub tx_mgr: Arc, + pub placements: Vec, +} + #[derive(Clone)] pub struct PCreateTable { pub tx_mgr: Arc, pub schema: SchemaTable, + pub partition_binding: Option, } #[derive(Clone)] diff --git a/mudu_runtime/Cargo.toml b/mudu_runtime/Cargo.toml index 5b257bf..f00a40a 100644 --- a/mudu_runtime/Cargo.toml +++ b/mudu_runtime/Cargo.toml @@ -44,13 +44,9 @@ base64 = { workspace = true } pgwire = { workspace = true } async-trait = { workspace = true } futures = { workspace = true } -turso = {version = "0.4.0-pre.19"} [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -postgres = { workspace = true } scc = { workspace = true } - - diff --git a/mudu_runtime/src/backend/backend.rs b/mudu_runtime/src/backend/backend.rs index 439be46..3949611 100644 --- a/mudu_runtime/src/backend/backend.rs +++ b/mudu_runtime/src/backend/backend.rs @@ -7,7 +7,7 @@ use crate::service::service::Service; use mudu::common::result::RS; use mudu::error::ec::EC; use mudu::m_error; -use mudu_utils::notifier::{Notifier, Waiter, notify_wait}; +use mudu_utils::notifier::{notify_wait, Notifier, Waiter}; use mudu_utils::sync::async_task::TaskWrapper; use std::net::SocketAddr; use std::str::FromStr; diff --git a/mudu_runtime/src/backend/http_api/io_uring_http_api.rs b/mudu_runtime/src/backend/http_api/io_uring_http_api.rs index 8fcab7e..b839870 100644 --- a/mudu_runtime/src/backend/http_api/io_uring_http_api.rs +++ b/mudu_runtime/src/backend/http_api/io_uring_http_api.rs @@ -1,22 +1,34 @@ use super::{ - AsyncIoUringInvokeClientFactory, HttpApi, ServerTopology, TokioIoUringInvokeClientFactory, - WorkerTopology, find_app, parse_json_object_body, to_param, + find_app, parse_json_object_body, to_param, AsyncIoUringInvokeClientFactory, HttpApi, + PartitionRouteEntry, PartitionRouteRequest, PartitionRouteResponse, ServerTopology, + TokioIoUringInvokeClientFactory, WorkerTopology, }; use crate::backend::app_mgr::AppMgr; use crate::backend::mududb_cfg::MuduDBCfg; use async_trait::async_trait; use mudu::common::result::RS; +use mudu_kernel::contract::meta_mgr::MetaMgr; use mudu::utils::json::JsonValue; use mudu_binding::procedure::procedure_invoke; use mudu_contract::procedure::proc_desc::ProcDesc; +use mudu_kernel::mudu_conn::mudu_conn_async::{ + set_default_remote_addr, set_default_remote_worker_id, +}; +use mudu_kernel::meta::meta_mgr_factory::MetaMgrFactory; +use mudu_kernel::server::partition_router::{ + PartitionRouter, DEFAULT_UNPARTITIONED_TABLE_PARTITION_ID, +}; use mudu_kernel::server::worker_registry::WorkerRegistry; use serde_json::Value; +use std::ops::Bound; use std::sync::Arc; pub struct IoUringHttpApi { app_mgr: Arc, tcp_addr: String, worker_registry: Arc, + meta_mgr: Arc, + partition_router: PartitionRouter, client_factory: Arc, } @@ -30,6 +42,8 @@ impl IoUringHttpApi { app_mgr, format!("{}:{}", cfg.listen_ip, cfg.tcp_listen_port), worker_registry, + MetaMgrFactory::create(cfg.db_path.clone()) + .unwrap_or_else(|e| panic!("create http meta manager failed: {e}")), Arc::new(TokioIoUringInvokeClientFactory), ) } @@ -38,15 +52,38 @@ impl IoUringHttpApi { app_mgr: Arc, tcp_addr: String, worker_registry: Arc, + meta_mgr: Arc, client_factory: Arc, ) -> Self { + set_default_remote_addr(Some(tcp_addr.clone())); + set_default_remote_worker_id(worker_registry.default_global_worker_id()); Self { app_mgr, tcp_addr, worker_registry, + partition_router: PartitionRouter::new(meta_mgr.clone()), + meta_mgr, client_factory, } } + + async fn resolve_partition_worker(&self, partition_id: mudu::common::id::OID) -> RS { + if let Some(worker_id) = self.meta_mgr.get_partition_worker(partition_id).await? { + return Ok(worker_id); + } + if partition_id == DEFAULT_UNPARTITIONED_TABLE_PARTITION_ID { + return self.worker_registry.default_global_worker_id().ok_or_else(|| { + mudu::m_error!( + mudu::error::ec::EC::NoSuchElement, + "worker registry has no default global worker" + ) + }); + } + Err(mudu::m_error!( + mudu::error::ec::EC::NoSuchElement, + format!("no worker placement for partition {}", partition_id) + )) + } } #[async_trait(?Send)] @@ -125,6 +162,65 @@ impl HttpApi for IoUringHttpApi { self.app_mgr.uninstall(app_name.as_bytes().to_vec()).await } + async fn route_partition(&self, request: PartitionRouteRequest) -> RS { + let rule = self + .meta_mgr + .get_partition_rule_by_name(&request.rule_name) + .await? + .ok_or_else(|| { + mudu::m_error!( + mudu::error::ec::EC::NoSuchElement, + format!("no such partition rule {}", request.rule_name) + ) + })?; + + let partition_ids = if let Some(key) = request.key { + if request.start.is_some() || request.end.is_some() { + return Err(mudu::m_error!( + mudu::error::ec::EC::ParseErr, + "partition route request cannot specify both key and range" + )); + } + vec![self + .partition_router + .route_rule_exact_partition( + &rule, + &key.into_iter().map(|value| value.into_bytes()).collect::>(), + )?] + } else { + let start = match request.start { + Some(values) => Bound::Included( + values + .into_iter() + .map(|value| value.into_bytes()) + .collect::>(), + ), + None => Bound::Unbounded, + }; + let end = match request.end { + Some(values) => Bound::Excluded( + values + .into_iter() + .map(|value| value.into_bytes()) + .collect::>(), + ), + None => Bound::Unbounded, + }; + self.partition_router + .route_rule_range_partitions(&rule, &start, &end)? + }; + + let mut routes = Vec::with_capacity(partition_ids.len()); + for partition_id in partition_ids { + let worker_id = self.resolve_partition_worker(partition_id).await?; + routes.push(PartitionRouteEntry { + partition_id, + worker_id, + }); + } + Ok(PartitionRouteResponse { routes }) + } + async fn invoke_json( &self, app_name: &str, diff --git a/mudu_runtime/src/backend/http_api/legacy_http_api.rs b/mudu_runtime/src/backend/http_api/legacy_http_api.rs index 482d1aa..19df831 100644 --- a/mudu_runtime/src/backend/http_api/legacy_http_api.rs +++ b/mudu_runtime/src/backend/http_api/legacy_http_api.rs @@ -1,6 +1,6 @@ use super::{ - HttpApi, legacy_invoke_async_proc, legacy_invoke_sync_proc, parse_json_object_body, - runtime_get_app_and_desc, + legacy_invoke_async_proc, legacy_invoke_sync_proc, parse_json_object_body, + runtime_get_app_and_desc, HttpApi, }; use crate::service::runtime::Runtime; use async_trait::async_trait; diff --git a/mudu_runtime/src/backend/http_api/mod.rs b/mudu_runtime/src/backend/http_api/mod.rs index 3477a30..df3106f 100644 --- a/mudu_runtime/src/backend/http_api/mod.rs +++ b/mudu_runtime/src/backend/http_api/mod.rs @@ -28,7 +28,7 @@ use crate::service::app_inst::AppInst; use crate::service::runtime::Runtime; use actix_cors::Cors; use actix_web::http::StatusCode; -use actix_web::{App, HttpResponse, HttpServer, Responder, delete, get, post, web}; +use actix_web::{delete, get, post, web, App, HttpResponse, HttpServer, Responder}; use async_trait::async_trait; use base64::Engine; use mudu::common::id::OID; @@ -102,6 +102,28 @@ pub struct ServerTopology { pub workers: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PartitionRouteRequest { + pub rule_name: String, + #[serde(default)] + pub key: Option>, + #[serde(default)] + pub start: Option>, + #[serde(default)] + pub end: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PartitionRouteEntry { + pub partition_id: mudu::common::id::OID, + pub worker_id: mudu::common::id::OID, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PartitionRouteResponse { + pub routes: Vec, +} + #[cfg(target_os = "linux")] use crate::backend::app_mgr::AppMgr; #[cfg(target_os = "linux")] @@ -128,6 +150,13 @@ pub trait HttpApi: Send + Sync { body: String, ) -> RS; + async fn route_partition(&self, _request: PartitionRouteRequest) -> RS { + Err(m_error!( + EC::NotImplemented, + "partition route is not supported" + )) + } + async fn server_topology(&self) -> RS { Err(m_error!( EC::NotImplemented, @@ -231,6 +260,7 @@ fn configure_routes(cfg: &mut web::ServiceConfig, capabilities: HttpApiCapabilit .service(app_proc_list) .service(app_proc_detail) .service(server_topology) + .service(partition_route) .service(install); if capabilities.enable_invoke { cfg.service(invoke); @@ -240,6 +270,35 @@ fn configure_routes(cfg: &mut web::ServiceConfig, capabilities: HttpApiCapabilit } } +#[post("/mudu/partition/route")] +async fn partition_route( + body: String, + context: web::Data, +) -> impl Responder { + let request = match serde_json::from_str::(&body) { + Ok(request) => request, + Err(e) => { + return HttpResponse::Ok().json(serde_json::json!({ + "status": 1001, + "message": "fail to parse partition route request", + "data": e.to_string(), + })) + } + }; + match context.api.route_partition(request).await { + Ok(route) => HttpResponse::Ok().json(serde_json::json!({ + "status": 0, + "message": "ok", + "data": route, + })), + Err(e) => HttpResponse::Ok().json(serde_json::json!({ + "status": 1001, + "message": "fail to route partition", + "data": e, + })), + } +} + #[get("/mudu/server/topology")] async fn server_topology(context: web::Data) -> impl Responder { match context.api.server_topology().await { @@ -514,17 +573,29 @@ async fn find_app(app_mgr: &dyn AppMgr, app_name: &str) -> RS { #[cfg(test)] mod test { use super::*; - use actix_web::{App, test}; + use actix_web::{test, App}; #[cfg(target_os = "linux")] use mudu::common::app_info::AppInfo; #[cfg(target_os = "linux")] + use mudu::common::id::gen_oid; + #[cfg(target_os = "linux")] use mudu_contract::procedure::mod_proc_desc::ModProcDesc; #[cfg(target_os = "linux")] use mudu_contract::procedure::procedure_result::ProcedureResult; use mudu_contract::tuple::tuple_datum::TupleDatum; #[cfg(target_os = "linux")] + use mudu_kernel::contract::partition_rule::{ + PartitionBound, PartitionRuleDesc, RangePartitionDef, + }; + #[cfg(target_os = "linux")] + use mudu_kernel::contract::partition_rule_binding::PartitionPlacement; + #[cfg(target_os = "linux")] use mudu_kernel::server::async_func_runtime::AsyncFuncInvoker; #[cfg(target_os = "linux")] + use mudu_kernel::meta::meta_mgr_factory::MetaMgrFactory; + #[cfg(target_os = "linux")] + use mudu_type::dat_type_id::DatTypeID; + #[cfg(target_os = "linux")] use std::sync::Mutex; struct MockHttpApi; @@ -629,6 +700,9 @@ mod test { } async fn close_session(&mut self, _session_id: u128) -> RS { + if self.closed { + return Err(m_error!(EC::IOErr, "close session failed")); + } self.closed = true; Ok(true) } @@ -637,6 +711,7 @@ mod test { #[cfg(target_os = "linux")] struct MockClientFactory { requests: Arc>>, + fail_close: bool, } #[cfg(target_os = "linux")] @@ -645,7 +720,7 @@ mod test { async fn connect(&self, _addr: &str) -> RS> { Ok(Box::new(MockClient { session_id: 9, - closed: false, + closed: self.fail_close, requests: self.requests.clone(), })) } @@ -707,8 +782,16 @@ mod test { Arc::new(MockAppMgr), "127.0.0.1:9527".to_string(), registry, + MetaMgrFactory::create( + std::env::temp_dir() + .join(format!("http_api_meta_test_{}", gen_oid())) + .to_string_lossy() + .to_string(), + ) + .unwrap(), Arc::new(MockClientFactory { requests: requests.clone(), + fail_close: false, }), ); @@ -724,4 +807,216 @@ mod test { &["app1/mod1/proc1".to_string()] ); } + + #[cfg(target_os = "linux")] + #[actix_web::test] + async fn iouring_http_api_routes_point_and_range_by_rule_name() { + let log_dir = + std::env::temp_dir().join(format!("http_api_route_test_{}", mudu::common::id::gen_oid())); + let registry = + mudu_kernel::server::worker_registry::load_or_create_worker_registry(&log_dir, 4) + .unwrap(); + let meta_dir = std::env::temp_dir().join(format!("http_api_route_meta_{}", gen_oid())); + let meta_mgr = MetaMgrFactory::create(meta_dir.to_string_lossy().to_string()).unwrap(); + + let rule = PartitionRuleDesc::new_range( + "global_rule".to_string(), + vec![DatTypeID::I32], + vec![ + RangePartitionDef::new( + "p0".to_string(), + PartitionBound::Unbounded, + PartitionBound::Value(vec![b"100".to_vec()]), + ), + RangePartitionDef::new( + "p1".to_string(), + PartitionBound::Value(vec![b"100".to_vec()]), + PartitionBound::Unbounded, + ), + ], + ); + let p0 = rule.partitions[0].partition_id; + let p1 = rule.partitions[1].partition_id; + let w0 = registry.workers()[0].worker_id; + let w1 = registry.workers()[1].worker_id; + meta_mgr.create_partition_rule(&rule).await.unwrap(); + meta_mgr + .upsert_partition_placements(&[ + PartitionPlacement { + partition_id: p0, + worker_id: w0, + }, + PartitionPlacement { + partition_id: p1, + worker_id: w1, + }, + ]) + .await + .unwrap(); + + let api = IoUringHttpApi::with_client_factory( + Arc::new(MockAppMgr), + "127.0.0.1:9527".to_string(), + registry, + meta_mgr, + Arc::new(MockClientFactory { + requests: Arc::new(Mutex::new(Vec::new())), + fail_close: false, + }), + ); + + let point = api + .route_partition(PartitionRouteRequest { + rule_name: "global_rule".to_string(), + key: Some(vec!["12".to_string()]), + start: None, + end: None, + }) + .await + .unwrap(); + assert_eq!(point.routes.len(), 1); + assert_eq!(point.routes[0].partition_id, p0); + assert_eq!(point.routes[0].worker_id, w0); + + let range = api + .route_partition(PartitionRouteRequest { + rule_name: "global_rule".to_string(), + key: None, + start: Some(vec!["50".to_string()]), + end: Some(vec!["150".to_string()]), + }) + .await + .unwrap(); + assert_eq!(range.routes.len(), 2); + assert_eq!(range.routes[0].partition_id, p0); + assert_eq!(range.routes[0].worker_id, w0); + assert_eq!(range.routes[1].partition_id, p1); + assert_eq!(range.routes[1].worker_id, w1); + } + + #[cfg(target_os = "linux")] + #[actix_web::test] + async fn iouring_http_api_lists_metadata_and_topology() { + let log_dir = + std::env::temp_dir().join(format!("http_api_meta_list_{}", mudu::common::id::gen_oid())); + let registry = + mudu_kernel::server::worker_registry::load_or_create_worker_registry(&log_dir, 3) + .unwrap(); + let meta_mgr = MetaMgrFactory::create( + std::env::temp_dir() + .join(format!("http_api_meta_mgr_{}", gen_oid())) + .to_string_lossy() + .to_string(), + ) + .unwrap(); + let api = IoUringHttpApi::with_client_factory( + Arc::new(MockAppMgr), + "127.0.0.1:9527".to_string(), + registry.clone(), + meta_mgr, + Arc::new(MockClientFactory { + requests: Arc::new(Mutex::new(Vec::new())), + fail_close: false, + }), + ); + + assert_eq!(api.list_apps().await.unwrap(), vec!["app1".to_string()]); + assert_eq!( + api.list_procedures("app1").await.unwrap(), + vec!["mod1/proc1".to_string()] + ); + let (desc, param_json, return_json) = api + .procedure_detail("app1", "mod1", "proc1") + .await + .unwrap(); + assert_eq!(desc.proc_name(), "proc1"); + assert_eq!(param_json["value"], 0); + assert_eq!(return_json["value"], 0); + + let topology = api.server_topology().await.unwrap(); + assert_eq!(topology.worker_count, registry.workers().len()); + assert_eq!(topology.workers.len(), registry.workers().len()); + } + + #[cfg(target_os = "linux")] + #[actix_web::test] + async fn iouring_http_api_surfaces_close_session_failure() { + let log_dir = + std::env::temp_dir().join(format!("http_api_close_err_{}", mudu::common::id::gen_oid())); + let registry = + mudu_kernel::server::worker_registry::load_or_create_worker_registry(&log_dir, 2) + .unwrap(); + let meta_mgr = MetaMgrFactory::create( + std::env::temp_dir() + .join(format!("http_api_close_meta_{}", gen_oid())) + .to_string_lossy() + .to_string(), + ) + .unwrap(); + let api = IoUringHttpApi::with_client_factory( + Arc::new(MockAppMgr), + "127.0.0.1:9527".to_string(), + registry, + meta_mgr, + Arc::new(MockClientFactory { + requests: Arc::new(Mutex::new(Vec::new())), + fail_close: true, + }), + ); + + let err = api + .invoke_json("app1", "mod1", "proc1", r#"{"value": 7}"#.to_string()) + .await + .unwrap_err(); + assert!(err.to_string().contains("close session failed")); + } + + #[cfg(target_os = "linux")] + #[actix_web::test] + async fn iouring_http_api_rejects_mixed_route_request_shapes() { + let log_dir = std::env::temp_dir() + .join(format!("http_api_route_shape_{}", mudu::common::id::gen_oid())); + let registry = + mudu_kernel::server::worker_registry::load_or_create_worker_registry(&log_dir, 2) + .unwrap(); + let meta_mgr = MetaMgrFactory::create( + std::env::temp_dir() + .join(format!("http_api_route_shape_meta_{}", gen_oid())) + .to_string_lossy() + .to_string(), + ) + .unwrap(); + let rule = PartitionRuleDesc::new_range( + "shape_rule".to_string(), + vec![DatTypeID::I32], + vec![RangePartitionDef::new( + "p0".to_string(), + PartitionBound::Unbounded, + PartitionBound::Unbounded, + )], + ); + meta_mgr.create_partition_rule(&rule).await.unwrap(); + + let api = IoUringHttpApi::with_client_factory( + Arc::new(MockAppMgr), + "127.0.0.1:9527".to_string(), + registry, + meta_mgr, + Arc::new(MockClientFactory { + requests: Arc::new(Mutex::new(Vec::new())), + fail_close: false, + }), + ); + + let err = api + .route_partition(PartitionRouteRequest { + rule_name: "shape_rule".to_string(), + key: Some(vec!["1".to_string()]), + start: Some(vec!["0".to_string()]), + end: None, + }) + .await + .unwrap_err(); + assert!(err.to_string().contains("cannot specify both key and range")); + } } diff --git a/mudu_runtime/src/backend/mod.rs b/mudu_runtime/src/backend/mod.rs index c926615..1b56d96 100644 --- a/mudu_runtime/src/backend/mod.rs +++ b/mudu_runtime/src/backend/mod.rs @@ -6,11 +6,9 @@ pub mod mududb_cfg; mod session; mod session_ctx; mod session_handle_task; -mod test_backend; -mod test_pg_cli; -mod test_sql; #[cfg(all(test, target_os = "linux"))] mod sql_async_client_test; +mod test_backend; pub mod web_handle_task; pub mod web_serve; diff --git a/mudu_runtime/src/backend/mudu_app_mgr.rs b/mudu_runtime/src/backend/mudu_app_mgr.rs index 322c0b6..c9d5084 100644 --- a/mudu_runtime/src/backend/mudu_app_mgr.rs +++ b/mudu_runtime/src/backend/mudu_app_mgr.rs @@ -8,6 +8,7 @@ use crate::service::runtime_opt::RuntimeOpt; use async_trait::async_trait; use mudu::common::id::OID; use mudu::common::result::RS; +use mudu::common::xid::INVALID_XID; use mudu::error::ec::EC; use mudu::m_error; use mudu_binding::procedure::procedure_invoke; @@ -69,7 +70,11 @@ impl AsyncFuncInvoker for MuduProcInvoker { let task_id = app.task_create().await?; let invoke_result = async { let mut param = procedure_invoke::deserialize_param(&procedure_parameters)?; - param.set_session_id(session_id); + let _ = session_id; + // TCP session ids belong to the transport/session manager. Procedure + // host syscalls require a database Context xid, so let the app + // runtime create one per invocation. + param.set_session_id(INVALID_XID); let result = if self.enable_async { app.invoke_async(task_id, &mod_name, &proc_name, param, Some(worker_local)) .await? @@ -209,6 +214,7 @@ async fn create_runtime_from_cfg(cfg: &MuduDBCfg) -> RS> { RuntimeOpt { component_target, enable_async, + sever_mode: cfg.server_mode, }, ) .await diff --git a/mudu_runtime/src/backend/sql_async_client_test.rs b/mudu_runtime/src/backend/sql_async_client_test.rs index 07a9080..3080d30 100644 --- a/mudu_runtime/src/backend/sql_async_client_test.rs +++ b/mudu_runtime/src/backend/sql_async_client_test.rs @@ -5,7 +5,9 @@ mod tests { use lazy_static::lazy_static; use mudu::common::result::RS; use mudu_cli::client::async_client::{AsyncClient, AsyncClientImpl}; - use mudu_contract::protocol::ClientRequest; + use mudu_contract::protocol::{ClientRequest, ServerResponse}; + use mudu_type::dat_type_id::DatTypeID; + use mudu_type::datum::DatumDyn; use mudu_utils::notifier::notify_wait; use std::fs; use std::net::TcpListener; @@ -71,7 +73,35 @@ mod tests { async fn with_timeout(future: impl std::future::Future>) -> RS { timeout(Duration::from_secs(20), future) .await - .map_err(|_| mudu::m_error!(mudu::error::ec::EC::TokioErr, "sql async client test timed out"))? + .map_err(|_| { + mudu::m_error!( + mudu::error::ec::EC::TokioErr, + "sql async client test timed out" + ) + })? + } + + fn response_rows_as_strings(response: &ServerResponse) -> Vec> { + response + .rows() + .iter() + .map(|row| { + row.values() + .iter() + .zip(response.row_desc().fields().iter()) + .map(|(value, field_desc)| { + if field_desc.dat_type().dat_type_id() == DatTypeID::String { + value.expect_string().clone() + } else { + value + .to_textual(field_desc.dat_type()) + .map(|text| text.to_string()) + .unwrap() + } + }) + .collect::>() + }) + .collect::>() } fn stop_server( @@ -81,16 +111,21 @@ mod tests { ) -> RS<()> { drop(client); stop_notifier.notify_all(); - server - .join() - .map_err(|_| mudu::m_error!(mudu::error::ec::EC::ThreadErr, "join sql async backend thread error"))? + server.join().map_err(|_| { + mudu::m_error!( + mudu::error::ec::EC::ThreadErr, + "join sql async backend thread error" + ) + })? } - async fn start_client_backend() -> Option>, - )>> { + async fn start_client_backend() -> Option< + RS<( + AsyncClientImpl, + mudu_utils::notifier::Notifier, + JoinHandle>, + )>, + > { let Some(cfg) = test_cfg() else { return None; }; @@ -129,40 +164,42 @@ mod tests { Err(err) => { stop_notifier.notify_all(); let _ = server.join(); - if err.to_string().contains("connect io_uring tcp server error") { + if err + .to_string() + .contains("connect io_uring tcp server error") + { return Ok(()); } return Err(err); } }; - with_timeout(client - .execute(ClientRequest::new( - "default", - "CREATE TABLE t(id INT, v INT, PRIMARY KEY(id))", - ))) + with_timeout(client.execute(ClientRequest::new( + "default", + "CREATE TABLE t(id INT, v INT, PRIMARY KEY(id))", + ))) .await?; - let inserted = with_timeout(client - .execute(ClientRequest::new( - "default", - "INSERT INTO t(id, v) VALUES (1, 10)", - ))) + let inserted = with_timeout(client.execute(ClientRequest::new( + "default", + "INSERT INTO t(id, v) VALUES (1, 10)", + ))) .await?; assert_eq!(inserted.affected_rows(), 1); - let selected = with_timeout(client - .query(ClientRequest::new( - "default", - "SELECT id, v FROM t WHERE id = 1", - ))) + let selected = with_timeout(client.query(ClientRequest::new( + "default", + "SELECT id, v FROM t WHERE id = 1", + ))) .await?; - assert_eq!(selected.rows(), &[vec!["1".to_string(), "10".to_string()]]); + assert_eq!( + response_rows_as_strings(&selected), + vec![vec!["1".to_string(), "10".to_string()]] + ); - let updated = with_timeout(client - .execute(ClientRequest::new( - "default", - "UPDATE t SET v = 20 WHERE id = 1", - ))) + let updated = with_timeout(client.execute(ClientRequest::new( + "default", + "UPDATE t SET v = 20 WHERE id = 1", + ))) .await?; assert_eq!(updated.affected_rows(), 1); @@ -171,13 +208,11 @@ mod tests { "SELECT v FROM t WHERE id = 1", ))) .await?; - assert_eq!(selected.rows(), &[vec!["20".to_string()]]); + assert_eq!(response_rows_as_strings(&selected), vec![vec!["20".to_string()]]); - let deleted = with_timeout(client - .execute(ClientRequest::new( - "default", - "DELETE FROM t WHERE id = 1", - ))) + let deleted = with_timeout( + client.execute(ClientRequest::new("default", "DELETE FROM t WHERE id = 1")), + ) .await?; assert_eq!(deleted.affected_rows(), 1); @@ -207,28 +242,32 @@ mod tests { Err(err) => { stop_notifier.notify_all(); let _ = server.join(); - if err.to_string().contains("connect io_uring tcp server error") { + if err + .to_string() + .contains("connect io_uring tcp server error") + { return Ok(()); } return Err(err); } }; - with_timeout(client - .batch(ClientRequest::new( - "default", - "CREATE TABLE t(id INT, v INT, PRIMARY KEY(id));\ + with_timeout(client.batch(ClientRequest::new( + "default", + "CREATE TABLE t(id INT, v INT, PRIMARY KEY(id));\ INSERT INTO t(id, v) VALUES (1, 11);", - ))) + ))) .await?; - let selected = with_timeout(client - .query(ClientRequest::new( - "default", - "SELECT id, v FROM t WHERE id = 1", - ))) + let selected = with_timeout(client.query(ClientRequest::new( + "default", + "SELECT id, v FROM t WHERE id = 1", + ))) .await?; - assert_eq!(selected.rows(), &[vec!["1".to_string(), "11".to_string()]]); + assert_eq!( + response_rows_as_strings(&selected), + vec![vec!["1".to_string(), "11".to_string()]] + ); stop_server(client, stop_notifier, server)?; Ok(()) @@ -291,8 +330,8 @@ mod tests { ))) .await?; assert_eq!( - selected.rows(), - &[ + response_rows_as_strings(&selected), + vec![ vec!["2".to_string(), "20".to_string()], vec!["3".to_string(), "30".to_string()], vec!["4".to_string(), "40".to_string()], @@ -305,11 +344,8 @@ mod tests { ))) .await?; assert_eq!( - selected.rows(), - &[ - vec!["3".to_string()], - vec!["4".to_string()], - ] + response_rows_as_strings(&selected), + vec![vec!["3".to_string()], vec!["4".to_string()]] ); let selected = with_timeout(client.query(ClientRequest::new( @@ -318,11 +354,8 @@ mod tests { ))) .await?; assert_eq!( - selected.rows(), - &[ - vec!["40".to_string()], - vec!["50".to_string()], - ] + response_rows_as_strings(&selected), + vec![vec!["40".to_string()], vec!["50".to_string()]] ); let selected = with_timeout(client.query(ClientRequest::new( @@ -337,7 +370,7 @@ mod tests { "SELECT id FROM t WHERE id >= 3 AND id <= 3", ))) .await?; - assert_eq!(selected.rows(), &[vec!["3".to_string()]]); + assert_eq!(response_rows_as_strings(&selected), vec![vec!["3".to_string()]]); let selected = with_timeout(client.query(ClientRequest::new( "default", @@ -345,11 +378,8 @@ mod tests { ))) .await?; assert_eq!( - selected.rows(), - &[ - vec!["1".to_string()], - vec!["2".to_string()], - ] + response_rows_as_strings(&selected), + vec![vec!["1".to_string()], vec!["2".to_string()]] ); let selected = with_timeout(client.query(ClientRequest::new( @@ -358,8 +388,8 @@ mod tests { ))) .await?; assert_eq!( - selected.rows(), - &[ + response_rows_as_strings(&selected), + vec![ vec!["20".to_string()], vec!["30".to_string()], vec!["40".to_string()], @@ -389,9 +419,10 @@ mod tests { ))) .await .expect_err("mixed equality and range predicate should be rejected"); - assert!(err - .to_string() - .contains("mixed equality and range predicates are not implemented")); + assert!( + err.to_string() + .contains("mixed equality and range predicates are not implemented") + ); stop_server(client, stop_notifier, server)?; Ok(()) diff --git a/mudu_runtime/src/backend/test_backend.rs b/mudu_runtime/src/backend/test_backend.rs index 16bfa58..e69de29 100644 --- a/mudu_runtime/src/backend/test_backend.rs +++ b/mudu_runtime/src/backend/test_backend.rs @@ -1,41 +0,0 @@ -#[cfg(test)] -pub mod tests { - use crate::backend::backend::Backend; - use crate::backend::mududb_cfg::MuduDBCfg; - use crate::service::test_wasm_mod_path::wasm_mod_path; - use mudu::common::result::RS; - use std::env::temp_dir; - use std::fs; - - fn test_db_path() -> String { - let tmp = temp_dir().join(format!( - "test_bakend_{}", - mudu_sys::random::next_uuid_v4_string() - )); - if !tmp.as_path().exists() { - fs::create_dir_all(tmp.as_path()).unwrap(); - } - tmp.to_str().unwrap().to_string() - } - - fn _cfg() -> MuduDBCfg { - let cfg = MuduDBCfg { - mpk_path: wasm_mod_path(), - db_path: test_db_path(), - listen_ip: "0.0.0.0".to_string(), - http_listen_port: 8000, - http_worker_threads: 1, - pg_listen_port: 5432, - component_target: None, - enable_async: false, - ..Default::default() - }; - cfg - } - - pub fn test_backend() -> RS<()> { - let cfg = _cfg(); - Backend::sync_serve(cfg)?; - Ok(()) - } -} diff --git a/mudu_runtime/src/backend/test_pg_cli.rs b/mudu_runtime/src/backend/test_pg_cli.rs deleted file mode 100644 index 712f699..0000000 --- a/mudu_runtime/src/backend/test_pg_cli.rs +++ /dev/null @@ -1,104 +0,0 @@ -#[cfg(test)] -pub mod test { - use postgres::{Client, NoTls}; - - use std::thread::sleep; - use std::time::Duration; - use tracing::{error, info}; - - #[allow(unused)] - enum TestResult { - Query(Result>, String>), - Command(Result), - } - pub struct TestSQL { - sql: String, - result: TestResult, - } - - #[allow(unused)] - impl TestSQL { - pub fn from_query(sql: String, result: Result>, String>) -> Self { - Self { - sql, - result: TestResult::Query(result), - } - } - - pub fn from_command(sql: String, result: Result) -> Self { - Self { - sql, - result: TestResult::Command(result), - } - } - } - - pub fn run_pg_client( - pg_host: String, - database: String, - user: String, - password: String, - vec_sql: Vec, - ) { - let mut client = loop { - let connect_str = format!( - "host={} dbname={} user={} password={}", - pg_host, database, user, password - ); - let r = Client::connect(&connect_str, NoTls); - match r { - Ok(c) => break c, - Err(e) => { - info!("{:?}, {:?}, {:?}", e, e.code(), e.as_db_error()); - sleep(Duration::from_millis(1000)); - } - } - }; - for stmt in vec_sql { - let sql = stmt.sql; - match stmt.result { - TestResult::Command(r_expected) => { - let r_executed = client.execute(&sql, &[]); - match (&r_executed, &r_expected) { - (Ok(rows_executed), Ok(rows_expected)) => { - assert_eq!(rows_executed, rows_expected); - info!("{} rows affected", rows_executed); - } - (Err(e), Err(_e)) => { - error!("{:?}, {:?}, {:?}", e, e.code(), e.as_db_error()); - } - _ => { - error!("mismatch result {:?}, {:?}", r_executed, r_expected); - panic!("error mismatch result"); - } - }; - } - TestResult::Query(r_expected) => { - let r_executed = client.query(&sql, &[]); - match (r_executed, r_expected) { - (Ok(rows_executed), Ok(_rows_expected)) => { - let rows: Vec<_> = rows_executed - .iter() - .map(|row| { - let mut vec = vec![]; - for i in 0..row.len() { - let s: String = row.get(i); - vec.push(s); - } - vec - }) - .collect(); - println!("{:?} result rows", rows); - } - (Err(e), Err(_e)) => { - error!("{:?}, {:?}, {:?}", e, e.code(), e.as_db_error()); - } - _ => { - panic!("error mismatch result"); - } - } - } - } - } - } -} diff --git a/mudu_runtime/src/backend/test_sql.rs b/mudu_runtime/src/backend/test_sql.rs deleted file mode 100644 index 89239e6..0000000 --- a/mudu_runtime/src/backend/test_sql.rs +++ /dev/null @@ -1,82 +0,0 @@ -#[cfg(test)] -mod test { - use crate::backend::test_backend::tests::test_backend; - use crate::backend::test_pg_cli::test::{TestSQL, run_pg_client}; - use mudu_utils::log::log_setup; - use std::thread::JoinHandle; - use tracing::info; - - //#[test] - #[allow(unused)] - fn run_test_sql() { - log_setup("info"); - let server = mudu_serve(); - let cli = pg_client(); - server.join().unwrap(); - cli.join().unwrap(); - info!("run_test_sql test success"); - } - - fn mudu_serve() -> JoinHandle<()> { - std::thread::spawn(|| { - test_backend().unwrap(); - }) - } - - fn pg_client() -> JoinHandle<()> { - let thd = std::thread::spawn(move || _run_pg_client()); - thd - } - - fn _run_pg_client() { - let test_sql = vec![ - TestSQL::from_command( - r#" - CREATE TABLE T1( - C1 INT, - C2 INT, - C3 CHAR (20), - C4 INT, - C5 VARCHAR (25), - C6 INT, - PRIMARY KEY (C1, C2) - ); - "# - .to_string(), - Ok(0), - ), - TestSQL::from_command( - r#" - INSERT INTO T1 (C1,C2,C3,C4,C5,C6) - VALUES (1,1,'aaabbbccc1', - 1,'1323456',1); - "# - .to_string(), - Ok(1), - ), - TestSQL::from_command( - r#" - INSERT INTO T1 (C3,C4,C5,C6, C2, C1) - VALUES ('aaabbbccc2', - 2,'13234562',2, 2, 2); - "# - .to_string(), - Ok(1), - ), - TestSQL::from_command( - r#" - SELECT C4, C1, C2, C3, C2, C5 FROM T1 WHERE C1 = 1 AND C2 = 1; - "# - .to_string(), - Ok(1), - ), - ]; - run_pg_client( - "localhost".to_string(), - "app1".to_string(), - "root".to_string(), - "root".to_string(), - test_sql, - ); - } -} diff --git a/mudu_runtime/src/backend/web_serve.rs b/mudu_runtime/src/backend/web_serve.rs index b0a35c4..f070774 100644 --- a/mudu_runtime/src/backend/web_serve.rs +++ b/mudu_runtime/src/backend/web_serve.rs @@ -22,6 +22,7 @@ pub async fn async_serve( let runtime_opt = RuntimeOpt { component_target, enable_async, + sever_mode: cfg.server_mode, }; let service = create_runtime_service( &cfg.mpk_path, diff --git a/mudu_runtime/src/db_connector.rs b/mudu_runtime/src/db_connector.rs index f5c020f..561eaf0 100644 --- a/mudu_runtime/src/db_connector.rs +++ b/mudu_runtime/src/db_connector.rs @@ -1,7 +1,5 @@ use crate::db_libsql::ls_conn::{create_ls_conn, db_conn_get_libsql_connection}; use crate::db_libsql_async::libsql_async_conn::create_libsql_async_conn; -use crate::db_postgres::pg_interactive_conn::create_pg_interactive_conn; -use crate::db_turso::turso_conn::create_turso_conn; use libsql::Connection; use mudu::common::result::RS; use mudu::error::ec::EC; @@ -17,11 +15,9 @@ pub struct DBConnector {} #[derive(EnumString)] enum DBType { - Postgres, LibSQL, - Turso, LibSQLAsync, - Mudu, + MuduDB, } impl DBConnector { @@ -29,7 +25,7 @@ impl DBConnector { let db_str_param = parse_db_connect_string(connect_string); let mut passing_param = Vec::new(); let mut opt_ddl_path = None; - let mut opt_db_type = Some(DBType::Postgres); + let mut opt_db_type = Some(DBType::LibSQL); let mut opt_db_path = None; let mut opt_app = None; for key_value in db_str_param { @@ -56,18 +52,15 @@ impl DBConnector { let ddl_path = opt_ddl_path.unwrap_or_else(|| String::default()); let app_name = opt_app.unwrap_or(String::default()); - let params = merge_to_string(passing_param); let db_path = match opt_db_path { Some(db_path) => db_path, None => return Err(m_error!(EC::DBInternalError, "no db path specified")), }; match opt_db_type { Some(db_type) => match db_type { - DBType::Postgres => create_pg_interactive_conn(¶ms, &ddl_path), DBType::LibSQL => create_ls_conn(&db_path, &app_name, &ddl_path), - DBType::Turso => create_turso_conn(&db_path, &app_name).await, DBType::LibSQLAsync => create_libsql_async_conn(&db_path, &app_name).await, - DBType::Mudu => create_mudu_conn().await, + DBType::MuduDB => create_mudu_conn().await, }, None => Err(m_error!(EC::ParseErr, "not a valid DB type")), } @@ -133,18 +126,6 @@ fn parse_db_connect_string(input: &str) -> Vec { result } -fn merge_to_string(vec: Vec) -> String { - let n = vec.len(); - let mut ret = String::new(); - for (i, s) in vec.iter().enumerate() { - ret.push_str(s); - if i != n { - ret.push_str(" "); - } - } - ret -} - #[cfg(test)] mod tests { use super::*; diff --git a/mudu_runtime/src/db_libsql_async/libsql_async_conn.rs b/mudu_runtime/src/db_libsql_async/libsql_async_conn.rs index f964419..fdc8fa6 100644 --- a/mudu_runtime/src/db_libsql_async/libsql_async_conn.rs +++ b/mudu_runtime/src/db_libsql_async/libsql_async_conn.rs @@ -21,14 +21,14 @@ pub async fn create_libsql_async_conn(db_path: &String, app_name: &String) -> RS } pub struct LibSQLAsyncConn { - turso: Arc>, + inner: Arc>, } impl LibSQLAsyncConn { async fn new(db_path: String) -> RS { let conn = LibSQLAsyncConnInner::new(db_path).await?; Ok(Self { - turso: Arc::new(Mutex::new(conn)), + inner: Arc::new(Mutex::new(conn)), }) } @@ -36,7 +36,7 @@ impl LibSQLAsyncConn { where F: AsyncFnOnce(MutexGuard) -> RS, { - let guard = self.turso.lock().await; + let guard = self.inner.lock().await; f(guard).await } } diff --git a/mudu_runtime/src/db_libsql_async/result_set.rs b/mudu_runtime/src/db_libsql_async/result_set.rs index 99b4aad..8820420 100644 --- a/mudu_runtime/src/db_libsql_async/result_set.rs +++ b/mudu_runtime/src/db_libsql_async/result_set.rs @@ -73,7 +73,7 @@ impl ResultSetInner { .map_err(|e| m_error!(EC::DBInternalError, "query result next", e))?; match opt_row { Some(row) => { - let items = turso_db_row_to_tuple_item(row, self.tuple_desc.fields())?; + let items = libsql_db_row_to_tuple_item(row, self.tuple_desc.fields())?; Ok(Some(items)) } None => { @@ -102,7 +102,7 @@ impl Drop for ResultSetInner { } } -fn turso_db_row_to_tuple_item(row: Row, item_desc: &[DatumDesc]) -> RS { +fn libsql_db_row_to_tuple_item(row: Row, item_desc: &[DatumDesc]) -> RS { let mut vec = vec![]; if row.column_count() as usize != item_desc.len() { return Err(m_error!(EC::FatalError, "column count mismatch")); diff --git a/mudu_runtime/src/db_postgres/ddl_item.sql b/mudu_runtime/src/db_postgres/ddl_item.sql deleted file mode 100644 index c527800..0000000 --- a/mudu_runtime/src/db_postgres/ddl_item.sql +++ /dev/null @@ -1,10 +0,0 @@ -DROP TABLE IF EXISTS item; -CREATE TABLE item -( - i_id int NOT NULL, - i_name varchar(24) NOT NULL, - i_price decimal(5, 2) NOT NULL, - i_data varchar(50) NOT NULL, - i_im_id int NOT NULL, - PRIMARY KEY (i_id) -); \ No newline at end of file diff --git a/mudu_runtime/src/db_postgres/mod.rs b/mudu_runtime/src/db_postgres/mod.rs deleted file mode 100644 index d147bda..0000000 --- a/mudu_runtime/src/db_postgres/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod pg_interactive_conn; -mod result_set_pg; -mod test_conn; -mod tx_pg; diff --git a/mudu_runtime/src/db_postgres/pg_interactive_conn.rs b/mudu_runtime/src/db_postgres/pg_interactive_conn.rs deleted file mode 100644 index 9c898e4..0000000 --- a/mudu_runtime/src/db_postgres/pg_interactive_conn.rs +++ /dev/null @@ -1,208 +0,0 @@ -use crate::db_postgres::result_set_pg::ResultSetPG; -use crate::db_postgres::tx_pg::TxPg; -use crate::resolver::schema_mgr::SchemaMgr; -use crate::resolver::sql_resolver::SQLResolver; -use mudu::common::result::RS; -use mudu::common::xid::XID; -use mudu::error::ec::EC; -use mudu::m_error; -use mudu_contract::database::db_conn::DBConnSync; -use mudu_contract::database::result_set::ResultSet; -use mudu_contract::database::sql::DBConn; -use mudu_contract::database::sql_params::SQLParams; -use mudu_contract::database::sql_stmt::SQLStmt; -use mudu_contract::tuple::datum_desc::DatumDesc; -use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; -#[cfg(not(target_arch = "wasm32"))] -use postgres::Client; -use sql_parser::ast::parser::SQLParser; -use sql_parser::ast::stmt_select::StmtSelect; -use sql_parser::ast::stmt_type::{StmtCommand, StmtType}; -use std::sync::{Arc, Mutex}; - -pub fn create_pg_interactive_conn(conn_str: &String, ddl_path: &String) -> RS { - Ok(DBConn::Sync(Arc::new(PGInteractive::new( - conn_str, ddl_path, - )?))) -} - -struct PGInteractive { - parser: SQLParser, - resolver: SQLResolver, - db_conn: Mutex<(Client, Option)>, -} - -impl DBConnSync for PGInteractive { - fn exec_silent(&self, _sql_text: &String) -> RS<()> { - Ok(()) - } - - fn begin_tx(&self) -> RS { - let mut conn = self.db_conn.lock().unwrap(); - let transaction = conn.0.transaction().unwrap(); - let xid = mudu_sys::random::uuid_v4().as_u128() as XID; - let r = TxPg::new(transaction, xid); - conn.1 = Some(r); - Ok(xid) - } - - fn rollback_tx(&self) -> RS<()> { - let mut conn = self.db_conn.lock().unwrap(); - if conn.1.is_some() { - let opt = Option::take(&mut conn.1); - let tx = opt.unwrap(); - tx.rollback()?; - } - Ok(()) - } - - fn commit_tx(&self) -> RS<()> { - let mut conn = self.db_conn.lock().unwrap(); - if conn.1.is_some() { - let opt = Option::take(&mut conn.1); - let tx = opt.unwrap(); - tx.commit()?; - } - Ok(()) - } - - fn query( - &self, - sql: &dyn SQLStmt, - param: &dyn SQLParams, - ) -> RS<(Arc, Arc)> { - self.query_inner(sql, param) - } - - fn command(&self, sql: &dyn SQLStmt, param: &dyn SQLParams) -> RS { - self.command_inner(sql, param) - } - - fn batch(&self, _sql: &dyn SQLStmt, _param: &dyn SQLParams) -> RS { - Err(m_error!( - EC::NotImplemented, - "batch syscall is only implemented for libsql backends" - )) - } -} - -impl PGInteractive { - fn new(conn_str: &String, ddl_path: &String) -> RS { - let schema_mgr = Self::build_schema_mgr_from_ddl_sql(ddl_path)?; - let r = Client::connect(conn_str, postgres::NoTls); - let client = match r { - Err(e) => { - panic!("{:?}", e); - } - Ok(c) => c, - }; - let conn = Self { - parser: SQLParser::new(), - resolver: SQLResolver::new(schema_mgr), - db_conn: Mutex::new((client, None)), - }; - Ok(conn) - } - - fn build_schema_mgr_from_ddl_sql(ddl_path: &String) -> RS { - SchemaMgr::load_from_ddl_path(ddl_path) - } - - fn query_inner( - &self, - sql: &dyn SQLStmt, - param: &dyn SQLParams, - ) -> RS<(Arc, Arc)> { - let sql_string = sql.to_sql_string(); - let stmt = self.parse_one_query(&sql_string)?; - let resolved = self.resolver.resolve_query(&stmt)?; - let projection = resolved.projection().clone(); - let row_desc = Arc::new(TupleFieldDesc::new(projection)); - let sql_string = Self::replace_placeholder(&sql_string, resolved.placeholder(), param)?; - let mut conn = self.db_conn.lock().unwrap(); - let rows = match &mut conn.1 { - None => conn.0.query(sql_string.as_str(), &[]).unwrap(), - Some(tx) => { - let x = tx.transaction(); - x.query(sql_string.as_str(), &[]).unwrap() - } - }; - - let result_set = ResultSetPG::new(row_desc.clone(), rows); - Ok((Arc::new(result_set), row_desc)) - } - - fn command_inner(&self, sql: &dyn SQLStmt, param: &dyn SQLParams) -> RS { - let sql_string = sql.to_sql_string(); - let stmt = self.parse_one_command(&sql_string)?; - let resolved = self.resolver.resolved_command(&stmt)?; - let sql = Self::replace_placeholder(&sql_string, resolved.placeholder(), param)?; - - let mut conn = self.db_conn.lock().unwrap(); - let rows = match &mut conn.1 { - None => conn.0.execute(sql.as_str(), &[]).unwrap(), - Some(tx) => { - let x = tx.transaction(); - x.execute(sql_string.as_str(), &[]).unwrap() - } - }; - Ok(rows as _) - } - - fn replace_placeholder( - sql_string: &String, - desc: &Vec, - param: &dyn SQLParams, - ) -> RS { - let placeholder_str = "?"; - let placeholder_str_len = placeholder_str.len(); - let vec_indices: Vec<_> = sql_string - .match_indices(placeholder_str) - .into_iter() - .collect(); - if desc.len() != param.size() as usize || desc.len() != vec_indices.len() { - return Err(m_error!( - EC::ParseErr, - "parameter and placeholder count mismatch" - )); - } - - let mut start_pos = 0; - let mut sql_after_replaced = "".to_string(); - for i in 0..desc.len() { - let _s = &sql_string[start_pos..vec_indices[i].0]; - sql_after_replaced.push_str(_s); - sql_after_replaced.push_str(" "); - let s = param - .get_idx_unchecked(i as u64) - .to_textual(desc[i].dat_type())?; - sql_after_replaced.push_str(s.as_str()); - sql_after_replaced.push_str(" "); - start_pos += _s.len() + placeholder_str_len; - } - if start_pos != sql_string.len() { - sql_after_replaced.push_str(&sql_string[start_pos..]); - } - sql_after_replaced.push_str(" "); - Ok(sql_after_replaced) - } - - fn parse_one_query(&self, _sql: &String) -> RS { - todo!() - } - - fn parse_one_command(&self, sql: &String) -> RS { - let stmt_list = self.parser.parse(sql)?; - if stmt_list.stmts().len() != 1 { - return Err(m_error!( - EC::ParseErr, - "SQL text must be one select statement" - )); - } - let stmt_command = stmt_list.into_stmts().pop().unwrap(); - match stmt_command { - StmtType::Command(command) => Ok(command), - _ => Err(m_error!(EC::ParseErr, "SQL must be command statement")), - } - } -} diff --git a/mudu_runtime/src/db_postgres/result_set_pg.rs b/mudu_runtime/src/db_postgres/result_set_pg.rs deleted file mode 100644 index 6503124..0000000 --- a/mudu_runtime/src/db_postgres/result_set_pg.rs +++ /dev/null @@ -1,75 +0,0 @@ -use mudu::common::result::RS; -use mudu_contract::database::result_set::ResultSet; -use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; -use mudu_contract::tuple::tuple_value::TupleValue; -use mudu_type::dat_type_id::DatTypeID; -use mudu_type::dat_value::DatValue; -#[cfg(not(target_arch = "wasm32"))] -use postgres::Row; -use std::sync::{Arc, Mutex}; - -pub struct ResultSetPG { - desc: Arc, - rows: Mutex>, -} - -impl ResultSetPG { - pub fn new(desc: Arc, rows: Vec) -> Self { - Self { - desc, - rows: Mutex::new(rows), - } - } -} -impl ResultSet for ResultSetPG { - fn next(&self) -> RS> { - let opt_row = self.rows.lock().unwrap().pop(); - match opt_row { - Some(row) => { - let mut tuple_row = vec![]; - for (i, d) in self.desc.fields().iter().enumerate() { - let id = d.dat_type_id(); - let datum = match id { - DatTypeID::I32 => { - let val: i32 = row.get(i); - DatValue::from_i32(val) - } - DatTypeID::I64 => { - let val: i64 = row.get(i); - DatValue::from_i64(val) - } - DatTypeID::U128 => { - let val: String = row.get(i); - let val = val.parse::().expect("postgres oid parse error"); - DatValue::from_u128(val) - } - DatTypeID::I128 => { - let val: String = row.get(i); - let val = val.parse::().expect("postgres i128 parse error"); - DatValue::from_i128(val) - } - DatTypeID::F32 => { - let val: f32 = row.get(i); - DatValue::from_f32(val) - } - DatTypeID::F64 => { - let val: f64 = row.get(i); - DatValue::from_f64(val) - } - DatTypeID::String => { - let val: String = row.get(i); - DatValue::from_string(val) - } - _ => { - panic!("unsupported type {:?}", id); - } - }; - - tuple_row.push(datum); - } - Ok(Some(TupleValue::from(tuple_row))) - } - None => Ok(None), - } - } -} diff --git a/mudu_runtime/src/db_postgres/test_conn.rs b/mudu_runtime/src/db_postgres/test_conn.rs deleted file mode 100644 index 49b06d0..0000000 --- a/mudu_runtime/src/db_postgres/test_conn.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(test)] -mod test {} diff --git a/mudu_runtime/src/db_postgres/tx_pg.rs b/mudu_runtime/src/db_postgres/tx_pg.rs deleted file mode 100644 index 4478e99..0000000 --- a/mudu_runtime/src/db_postgres/tx_pg.rs +++ /dev/null @@ -1,58 +0,0 @@ -use mudu::common::result::RS; -use mudu::common::xid::XID; -use mudu_contract::database::sql::Context; -use mudu_contract::database::tx::Tx; -#[cfg(not(target_arch = "wasm32"))] -use postgres::Transaction; -use std::mem::ManuallyDrop; - -pub struct TxPg { - xid: XID, - transaction: ManuallyDrop>, -} - -unsafe fn extend_lifetime<'b>(r: Transaction<'b>) -> Transaction<'static> { - unsafe { std::mem::transmute::, Transaction<'static>>(r) } -} - -impl TxPg { - pub fn new<'a>(conn: Transaction<'a>, xid: XID) -> Self { - unsafe { - Self { - xid, - transaction: ManuallyDrop::new(extend_lifetime(conn)), - } - } - } - - pub fn commit(mut self) -> RS<()> { - let t = unsafe { ManuallyDrop::take(&mut self.transaction) }; - t.commit().unwrap(); - Ok(()) - } - - pub fn rollback(mut self) -> RS<()> { - let t = unsafe { ManuallyDrop::take(&mut self.transaction) }; - t.rollback().unwrap(); - Ok(()) - } - - pub fn transaction(&mut self) -> &mut Transaction<'static> { - &mut self.transaction - } -} - -impl Tx for TxPg { - fn xid(&self) -> XID { - self.xid - } -} - -impl Drop for TxPg { - fn drop(&mut self) { - unsafe { - ManuallyDrop::drop(&mut self.transaction); - } - let _ = Context::remove(self.xid); - } -} diff --git a/mudu_runtime/src/db_turso/mod.rs b/mudu_runtime/src/db_turso/mod.rs deleted file mode 100644 index 3b240a8..0000000 --- a/mudu_runtime/src/db_turso/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[allow(unused)] -mod turso_conn_inner; - -mod param; -mod result_set; -pub mod turso_conn; -mod turso_desc; diff --git a/mudu_runtime/src/db_turso/param.rs b/mudu_runtime/src/db_turso/param.rs deleted file mode 100644 index 3f6ebef..0000000 --- a/mudu_runtime/src/db_turso/param.rs +++ /dev/null @@ -1,21 +0,0 @@ -use turso; - -pub struct TursoParam { - inner: Vec, -} - -impl TursoParam { - pub fn new(inner: Vec) -> Self { - let mut inner = inner; - inner.reverse(); - Self { inner } - } -} - -impl Iterator for TursoParam { - type Item = turso::Value; - - fn next(&mut self) -> Option { - self.inner.pop() - } -} diff --git a/mudu_runtime/src/db_turso/result_set.rs b/mudu_runtime/src/db_turso/result_set.rs deleted file mode 100644 index 4266dbc..0000000 --- a/mudu_runtime/src/db_turso/result_set.rs +++ /dev/null @@ -1,169 +0,0 @@ -use async_trait::async_trait; -use mudu::common::result::RS; -use mudu::error::ec::EC; -use mudu::m_error; -use mudu_contract::database::result_set::ResultSetAsync; -use mudu_contract::tuple::datum_desc::DatumDesc; -use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; -use mudu_contract::tuple::tuple_value::TupleValue; -use mudu_type::dat_type_id::DatTypeID; -use mudu_type::dat_value::DatValue; -use std::sync::Arc; -use std::sync::Mutex as StdMutex; -use tokio::sync::Mutex; -use turso::{Row, Rows}; - -pub trait ResultSetLease: Send + Sync { - fn release(self: Box); -} - -pub struct TursoResultSet { - inner: Arc, -} - -pub struct ResultSetInner { - row: Mutex, - tuple_desc: Arc, - lease: StdMutex>>, -} - -impl TursoResultSet { - pub fn new( - rows: Rows, - desc: Arc, - lease: Option>, - ) -> TursoResultSet { - let inner = ResultSetInner::new(rows, desc, lease); - Self { - inner: Arc::new(inner), - } - } -} - -#[async_trait] -impl ResultSetAsync for TursoResultSet { - async fn next(&self) -> RS> { - self.inner.async_next().await - } - - fn desc(&self) -> &TupleFieldDesc { - &self.inner.tuple_desc.as_ref() - } -} - -impl ResultSetInner { - fn new( - row: Rows, - tuple_desc: Arc, - lease: Option>, - ) -> ResultSetInner { - Self { - row: Mutex::new(row), - tuple_desc, - lease: StdMutex::new(lease), - } - } - - async fn async_next(&self) -> RS> { - let mut guard = self.row.lock().await; - let opt_row = guard - .next() - .await - .map_err(|e| m_error!(EC::DBInternalError, "query result next", e))?; - match opt_row { - Some(row) => { - let items = turso_db_row_to_tuple_item(row, self.tuple_desc.fields())?; - Ok(Some(items)) - } - None => { - self.release_lease(); - Ok(None) - } - } - } - - fn release_lease(&self) { - if let Ok(mut guard) = self.lease.lock() { - if let Some(lease) = guard.take() { - lease.release(); - } - } - } -} - -impl Drop for ResultSetInner { - fn drop(&mut self) { - if let Ok(mut guard) = self.lease.lock() { - if let Some(lease) = guard.take() { - lease.release(); - } - } - } -} - -fn turso_db_row_to_tuple_item(row: Row, item_desc: &[DatumDesc]) -> RS { - let mut vec = vec![]; - if row.column_count() != item_desc.len() { - return Err(m_error!(EC::FatalError, "column count mismatch")); - } - for i in 0..item_desc.len() { - let desc = &item_desc[i]; - let n = i; - let internal = match desc.dat_type_id() { - DatTypeID::I32 => { - let val = row.get::(n).map_err(|e| { - m_error!(EC::DBInternalError, "turso db get item of row error", e) - })?; - DatValue::from_i32(val) - } - DatTypeID::I64 => { - let val = row.get::(n).map_err(|e| { - m_error!(EC::DBInternalError, "turso db get item of row error", e) - })?; - DatValue::from_i64(val) - } - DatTypeID::U128 => { - let val = row.get::(n).map_err(|e| { - m_error!(EC::DBInternalError, "turso db get item of row error", e) - })?; - let val = val - .parse::() - .map_err(|e| m_error!(EC::DBInternalError, "turso db oid parse error", e))?; - DatValue::from_u128(val) - } - DatTypeID::I128 => { - let val = row.get::(n).map_err(|e| { - m_error!(EC::DBInternalError, "turso db get item of row error", e) - })?; - let val = val - .parse::() - .map_err(|e| m_error!(EC::DBInternalError, "turso db i128 parse error", e))?; - DatValue::from_i128(val) - } - DatTypeID::F32 => { - let val = row.get::(n).map_err(|e| { - m_error!(EC::DBInternalError, "turso db get item of row error", e) - })?; - DatValue::from_f64(val) - } - DatTypeID::F64 => { - let val = row.get::(n).map_err(|_e| { - m_error!(EC::DBInternalError, "turso db get item of row error") - })?; - DatValue::from_f64(val) - } - DatTypeID::String => { - let val = row - .get::(n) - .map_err(|e| m_error!(EC::DBInternalError, "get item of row error", e))?; - DatValue::from_string(val) - } - _ => { - panic!("unsupported type {:?}", desc); - } - }; - - vec.push(internal) - } - Ok(TupleValue::from(vec)) -} diff --git a/mudu_runtime/src/db_turso/turso_conn.rs b/mudu_runtime/src/db_turso/turso_conn.rs deleted file mode 100644 index cd41f09..0000000 --- a/mudu_runtime/src/db_turso/turso_conn.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::db_turso::turso_conn_inner::TursoConnInner; -use async_trait::async_trait; -use mudu::common::result::RS; -use mudu::common::result_of::rs_option; -use mudu::common::xid::XID; -use mudu::error::ec::EC; -use mudu::m_error; -use mudu_contract::database::db_conn::DBConnAsync; -use mudu_contract::database::prepared_stmt::PreparedStmt; -use mudu_contract::database::result_set::ResultSetAsync; -use mudu_contract::database::sql::DBConn; -use mudu_contract::database::sql_params::SQLParams; -use mudu_contract::database::sql_stmt::SQLStmt; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::{Mutex, MutexGuard}; - -pub async fn create_turso_conn(db_path: &String, app_name: &String) -> RS { - let db_file_path = PathBuf::from(db_path).join(app_name); - let path = rs_option(db_file_path.to_str(), "path to string error")?.to_string(); - let conn = TursoConn::new(path).await?; - Ok(DBConn::Async(Arc::new(conn))) -} - -pub struct TursoConn { - turso: Arc>, -} - -impl TursoConn { - async fn new(db_path: String) -> RS { - let conn = TursoConnInner::new(db_path).await?; - Ok(Self { - turso: Arc::new(Mutex::new(conn)), - }) - } - - async fn handle_inner(&self, f: F) -> RS - where - F: AsyncFnOnce(MutexGuard) -> RS, - { - let guard = self.turso.lock().await; - f(guard).await - } -} - -#[async_trait] -impl DBConnAsync for TursoConn { - async fn prepare(&self, stmt: Box) -> RS> { - self.handle_inner(async move |inner: MutexGuard| inner.prepare(stmt).await) - .await - } - - async fn exec_silent(&self, sql_text: String) -> RS<()> { - self.handle_inner(async move |inner: MutexGuard| { - inner.exec_silent(sql_text).await - }) - .await - } - - async fn begin_tx(&self) -> RS { - self.handle_inner(async |mut inner: MutexGuard| inner.begin_tx().await) - .await - } - - async fn rollback_tx(&self) -> RS<()> { - self.handle_inner(async |mut inner: MutexGuard| inner.rollback_tx().await) - .await - } - - async fn commit_tx(&self) -> RS<()> { - self.handle_inner(async |mut inner: MutexGuard| inner.commit_tx().await) - .await - } - - async fn query( - &self, - sql: Box, - param: Box, - ) -> RS> { - let f = async move |inner: MutexGuard| inner.query(sql, param).await; - self.handle_inner(f).await - } - - async fn execute(&self, sql: Box, param: Box) -> RS { - let f = async move |inner: MutexGuard| inner.command(sql, param).await; - self.handle_inner(f).await - } - - async fn batch(&self, _sql: Box, _param: Box) -> RS { - Err(m_error!( - EC::NotImplemented, - "batch syscall is only implemented for libsql backends" - )) - } -} diff --git a/mudu_runtime/src/db_turso/turso_conn_inner.rs b/mudu_runtime/src/db_turso/turso_conn_inner.rs deleted file mode 100644 index 39488da..0000000 --- a/mudu_runtime/src/db_turso/turso_conn_inner.rs +++ /dev/null @@ -1,379 +0,0 @@ -use crate::db_turso::param::TursoParam; -use crate::db_turso::result_set::{ResultSetLease, TursoResultSet}; -use crate::db_turso::turso_desc::desc_projection; -use async_trait::async_trait; -use futures::TryFutureExt; -use lazy_static::lazy_static; -use mudu::common::result::RS; -use mudu::common::xid::{XID, new_xid}; -use mudu::error::ec::EC; -use mudu::error::err::MError; -use mudu::m_error; -use mudu_contract::database::db_conn::DBConnSync; -use mudu_contract::database::prepared_stmt::PreparedStmt; -use mudu_contract::database::result_set::ResultSetAsync; -use mudu_contract::database::sql_params::SQLParams; -use mudu_contract::database::sql_stmt::SQLStmt; -use mudu_contract::tuple::tuple_field_desc::TupleFieldDesc; -use mudu_type::dat_type::DatType; -use mudu_type::dat_type_id::DatTypeID; -use mudu_type::dat_value::DatValue; -use mudu_type::datum::{Datum, DatumDyn}; -use scc::HashMap as SCCHashMap; -use std::collections::HashMap; -use std::error::Error; -use std::sync::{Arc, Mutex as StdMutex}; -use turso::{Builder, Connection, Database, Statement, params_from_iter, transaction::Transaction}; - -pub fn create_turso_conn( - _db_path: &String, - _app_name: &String, - _ddl_path: &String, -) -> RS> { - todo!() -} - -pub struct TursoConnInner { - conn: Connection, - trans: Option>, - xid: XID, - cached_prepared: Arc>>, -} - -lazy_static! { - static ref TURSO_DB: SCCHashMap = SCCHashMap::new(); -} - -fn to_static_unsafe<'conn>(s: Transaction<'conn>) -> Transaction<'static> { - unsafe { std::mem::transmute::, Transaction<'static>>(s) } -} - -async fn get_db(db_path: &String) -> RS { - let opt_db = TURSO_DB.get_async(db_path).await; - match opt_db { - Some(db) => Ok(db.clone()), - None => { - let db = Builder::new_local(db_path) - .build() - .await - .map_err(|e| m_error!(EC::IOErr, format!("open database error {}", db_path), e))?; - let _ = TURSO_DB.insert_async(db_path.clone(), db.clone()).await; - Ok(db) - } - } -} - -impl TursoConnInner { - pub async fn new(db_path: String) -> RS { - let db = get_db(&db_path).await?; - let connection = db - .connect() - .map_err(|e| m_error!(EC::IOErr, format!("connect db error {}", db_path), e))?; - Ok(Self { - conn: connection, - trans: None, - xid: 0, - cached_prepared: Arc::new(StdMutex::new(HashMap::new())), - }) - } - - fn add_prepared(&self, sql: String, prepared: Prepared) { - let mut guard = self.cached_prepared.lock().unwrap(); - guard.insert(sql, prepared); - } - - async fn prepared(&self, sql: String, query: bool) -> RS<(String, Prepared)> { - let opt = { - let mut guard = self.cached_prepared.lock().unwrap(); - guard.remove(&sql) - }; - match opt { - Some(prepared) => Ok((sql, prepared)), - None => { - let stmt = self.conn.prepare(&sql).await.map_err(db_error)?; - let prepared = if query { - Prepared::new_query_stmt(sql.clone(), stmt).await? - } else { - Prepared::new_command_stmt(sql.clone(), stmt).await? - }; - Ok((sql, prepared)) - } - } - } -} - -pub struct Prepared { - sql: String, - stmt: Statement, - project_tuple_desc: Arc, -} - -pub struct PreparedStmtImpl { - prepared: Arc>>, -} - -#[async_trait] -impl PreparedStmt for PreparedStmtImpl { - async fn query(&self, params: Box) -> RS> { - let prepared = self.take_prepared()?; - let mut lease = PreparedSlotLease { - slot: self.prepared.clone(), - prepared: Some(prepared), - }; - let prepared = lease.prepared.take().unwrap(); - prepared.query_with_lease(params, Box::new(lease)).await - } - - async fn execute(&self, params: Box) -> RS { - let mut prepared = self.take_prepared()?; - let result = prepared.execute(params).await; - self.restore_prepared(prepared)?; - result - } - - async fn desc(&self) -> RS> { - let guard = self - .prepared - .lock() - .map_err(|_| m_error!(EC::MutexError, "lock prepared stmt error"))?; - let prepared = guard - .as_ref() - .ok_or_else(|| m_error!(EC::ExistingSuchElement, "prepared query is still in use"))?; - Ok(prepared.project_tuple_desc()) - } - - async fn reset(&self) -> RS<()> { - let mut guard = self - .prepared - .lock() - .map_err(|_| m_error!(EC::MutexError, "lock prepared stmt error"))?; - let prepared = guard - .as_mut() - .ok_or_else(|| m_error!(EC::ExistingSuchElement, "prepared query is still in use"))?; - prepared.reset(); - Ok(()) - } -} - -impl PreparedStmtImpl { - fn take_prepared(&self) -> RS { - self.prepared - .lock() - .map_err(|_| m_error!(EC::MutexError, "lock prepared stmt error"))? - .take() - .ok_or_else(|| { - m_error!( - EC::ExistingSuchElement, - "prepared statement is still in use" - ) - }) - } - - fn restore_prepared(&self, prepared: Prepared) -> RS<()> { - let mut guard = self - .prepared - .lock() - .map_err(|_| m_error!(EC::MutexError, "lock prepared stmt error"))?; - *guard = Some(prepared); - Ok(()) - } -} - -impl Prepared { - async fn query_with_lease( - mut self, - params: Box, - lease: Box, - ) -> RS> { - let turso_param = to_turso_params(params.as_ref())?; - let rows = self - .stmt - .query(params_from_iter(turso_param)) - .await - .map_err(db_error)?; - let desc = self.project_tuple_desc.clone(); - Ok(Arc::new(TursoResultSet::new(rows, desc, Some(lease)))) - } - - async fn execute(&mut self, params: Box) -> RS { - let turso_param = to_turso_params(params.as_ref())?; - let rows = self - .stmt - .execute(params_from_iter(turso_param)) - .await - .map_err(db_error)?; - self.stmt.reset(); - Ok(rows) - } - - pub fn project_tuple_desc(&self) -> Arc { - self.project_tuple_desc.clone() - } - - async fn new_query_stmt(sql: String, stmt: Statement) -> RS { - let desc = desc_projection(&stmt).await?; - Ok(Self { - sql, - stmt, - project_tuple_desc: Arc::new(TupleFieldDesc::new(desc)), - }) - } - - async fn new_command_stmt(sql: String, stmt: Statement) -> RS { - Ok(Self { - sql, - stmt, - project_tuple_desc: Arc::new(TupleFieldDesc::new(Vec::new())), - }) - } - - fn reset(&mut self) { - self.stmt.reset(); - } -} - -fn db_error(e: E) -> MError { - let detail = e.to_string(); - m_error!(EC::IOErr, format!("db error: {}", detail), e) -} - -fn to_turso_params(sql_param: &dyn SQLParams) -> RS { - let desc = sql_param.param_tuple_desc()?; - if desc.fields().len() as u64 != sql_param.size() { - return Err(m_error!( - EC::DBInternalError, - "parameter and description mismatch" - )); - } - let n = sql_param.size(); - let mut vec = Vec::with_capacity(n as usize); - for i in 0..n { - let datum = sql_param.get_idx_unchecked(i); - let desc = &desc.fields()[i as usize]; - let value = datum.to_value(desc.dat_type())?; - let turso_value = _to_turso_value(&value, desc.dat_type())?; - vec.push(turso_value); - } - Ok(TursoParam::new(vec)) -} - -fn _to_turso_value(datum: &DatValue, ty: &DatType) -> RS { - let id = ty.dat_type_id(); - let v = match id { - DatTypeID::I32 => turso::Value::Integer(datum.expect_i32().clone() as _), - DatTypeID::I64 => turso::Value::Integer(datum.expect_i64().clone() as _), - DatTypeID::U128 => turso::Value::Text(datum.expect_u128().to_string()), - DatTypeID::I128 => turso::Value::Text(datum.expect_i128().to_string()), - DatTypeID::F32 => turso::Value::Real(datum.expect_f32().clone() as _), - DatTypeID::F64 => turso::Value::Real(datum.expect_f64().clone() as _), - DatTypeID::String => turso::Value::Text(datum.expect_string().clone()), - DatTypeID::Array => turso::Value::Blob(datum.to_binary(ty)?.into()), - DatTypeID::Record => turso::Value::Blob(datum.to_binary(ty)?.into()), - DatTypeID::Binary => turso::Value::Blob(datum.to_binary(ty)?.into()), - }; - Ok(v) -} - -impl TursoConnInner { - pub async fn exec_silent(&self, sql_text: String) -> RS<()> { - let _ = self.conn.execute(&sql_text, ()).await.map_err(db_error)?; - Ok(()) - } - - pub async fn begin_tx(&mut self) -> RS { - let trans = self.conn.transaction().await.map_err(db_error)?; - self.trans = Some(to_static_unsafe(trans)); - self.xid = new_xid(); - Ok(self.xid) - } - - pub fn move_tx<'conn>(&mut self) -> Option> { - let mut trans = None; - std::mem::swap(&mut self.trans, &mut trans); - trans - } - - pub async fn rollback_tx(&mut self) -> RS<()> { - let opt_trans = self.move_tx(); - match opt_trans { - Some(trans) => trans.rollback().await.map_err(db_error), - None => Ok(()), - } - } - - pub async fn commit_tx(&mut self) -> RS<()> { - let opt_trans = self.move_tx(); - match opt_trans { - Some(trans) => trans.commit().await.map_err(db_error), - None => Ok(()), - } - } - - pub async fn prepare(&self, sql_stmt: Box) -> RS> { - let sql_str = sql_stmt.to_string(); - let (_, prepared) = self.prepared(sql_str, true).await?; - Ok(Arc::new(PreparedStmtImpl { - prepared: Arc::new(StdMutex::new(Some(prepared))), - })) - } - - pub async fn query( - &self, - sql_stmt: Box, - sql_params: Box, - ) -> RS> { - let sql_str = sql_stmt.to_string(); - let (sql, prepared) = self.prepared(sql_str, true).await?; - let mut lease = CachedPreparedLease { - sql, - cache: self.cached_prepared.clone(), - prepared: Some(prepared), - }; - let prepared = lease.prepared.take().unwrap(); - prepared.query_with_lease(sql_params, Box::new(lease)).await - } - - pub async fn command( - &self, - sql_stmt: Box, - sql_params: Box, - ) -> RS { - let sql = sql_stmt.to_string(); - let (sql, mut prepared) = self.prepared(sql, false).await?; - let result = prepared.execute(sql_params).await; - self.add_prepared(sql, prepared); - result - } -} - -struct CachedPreparedLease { - sql: String, - cache: Arc>>, - prepared: Option, -} - -impl ResultSetLease for CachedPreparedLease { - fn release(mut self: Box) { - if let Some(mut prepared) = self.prepared.take() { - prepared.reset(); - let mut guard = self.cache.lock().unwrap(); - guard.insert(self.sql.clone(), prepared); - } - } -} - -struct PreparedSlotLease { - slot: Arc>>, - prepared: Option, -} - -impl ResultSetLease for PreparedSlotLease { - fn release(mut self: Box) { - if let Some(mut prepared) = self.prepared.take() { - prepared.reset(); - if let Ok(mut guard) = self.slot.lock() { - *guard = Some(prepared); - } - } - } -} diff --git a/mudu_runtime/src/db_turso/turso_desc.rs b/mudu_runtime/src/db_turso/turso_desc.rs deleted file mode 100644 index 14eeef1..0000000 --- a/mudu_runtime/src/db_turso/turso_desc.rs +++ /dev/null @@ -1,43 +0,0 @@ -use mudu::common::result::RS; -use mudu::error::ec::EC; -use mudu::error::err::MError; -use mudu::m_error; -use mudu_contract::tuple::datum_desc::DatumDesc; -use mudu_type::dat_type::DatType; -use mudu_type::dat_type_id::DatTypeID; -use turso::Statement; - -/// Get schema information for a SQL query result set -/// This function executes the query with LIMIT 0 to get only the structure without data -pub async fn desc_projection(stmt: &Statement) -> Result, MError> { - let columns = stmt.columns(); - let mut schema = Vec::with_capacity(columns.len()); - for column in columns { - let type_str = column.decl_type().map_or_else( - || Err(m_error!(EC::NoneErr, "cannot get column type")), - |t| Ok(t.to_string()), - )?; - let id = sqlite_decl_type_to_id(&type_str)?; - let desc = DatumDesc::new(column.name().to_string(), DatType::default_for(id)); - - schema.push(desc); - } - - Ok(schema) -} - -fn sqlite_decl_type_to_id(name: &str) -> RS { - let id = match name { - "TEXT" => DatTypeID::String, - "INT" | "INTEGER" => DatTypeID::I32, - "BIGINT" => DatTypeID::I64, - "REAL" => DatTypeID::F64, - _ => { - return Err(m_error!( - EC::TypeErr, - format!("do not support type {}", name) - )); - } - }; - Ok(id) -} diff --git a/mudu_runtime/src/lib.rs b/mudu_runtime/src/lib.rs index 1944214..b67dc5d 100644 --- a/mudu_runtime/src/lib.rs +++ b/mudu_runtime/src/lib.rs @@ -3,8 +3,6 @@ pub mod backend; pub mod db_connector; mod db_libsql; mod db_libsql_async; -mod db_postgres; -mod db_turso; pub mod interface; mod procedure; pub mod resolver; diff --git a/mudu_runtime/src/resolver/filter.rs b/mudu_runtime/src/resolver/filter.rs deleted file mode 100644 index 4f20488..0000000 --- a/mudu_runtime/src/resolver/filter.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::resolver::item_value::ItemValue; -use sql_parser::ast::expr_operator::ValueCompare; - -pub struct Filter { - value_compare: ValueCompare, - filter_value: ItemValue, -} - -impl Filter { - pub fn new(value_compare: ValueCompare, filter_value: ItemValue) -> Self { - Self { - value_compare, - filter_value, - } - } - - pub fn compare_op(&self) -> ValueCompare { - self.value_compare - } - - pub fn filter_value(&self) -> &ItemValue { - &self.filter_value - } -} diff --git a/mudu_runtime/src/resolver/item_value.rs b/mudu_runtime/src/resolver/item_value.rs deleted file mode 100644 index 395e14b..0000000 --- a/mudu_runtime/src/resolver/item_value.rs +++ /dev/null @@ -1,6 +0,0 @@ -use mudu_type::dat_typed::DatTyped; - -pub enum ItemValue { - Literal(DatTyped), - Placeholder, -} diff --git a/mudu_runtime/src/resolver/mod.rs b/mudu_runtime/src/resolver/mod.rs index 9e6f915..a023081 100644 --- a/mudu_runtime/src/resolver/mod.rs +++ b/mudu_runtime/src/resolver/mod.rs @@ -1,9 +1 @@ -mod filter; -pub mod item_value; -pub mod resolved_command; -pub mod resolved_insert; -pub mod resolved_select; -pub mod resolved_type; -pub mod resolved_update; pub mod schema_mgr; -pub mod sql_resolver; diff --git a/mudu_runtime/src/resolver/resolved_command.rs b/mudu_runtime/src/resolver/resolved_command.rs deleted file mode 100644 index 7749efd..0000000 --- a/mudu_runtime/src/resolver/resolved_command.rs +++ /dev/null @@ -1,5 +0,0 @@ -use mudu_contract::tuple::datum_desc::DatumDesc; - -pub trait ResolvedCommand { - fn placeholder(&self) -> &Vec; -} diff --git a/mudu_runtime/src/resolver/resolved_insert.rs b/mudu_runtime/src/resolver/resolved_insert.rs deleted file mode 100644 index 9012c01..0000000 --- a/mudu_runtime/src/resolver/resolved_insert.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::resolver::item_value::ItemValue; -use crate::resolver::resolved_command::ResolvedCommand; -use mudu_contract::tuple::datum_desc::DatumDesc; - -pub struct ResolvedInsert { - insert_value: Vec<(DatumDesc, ItemValue)>, - placeholder: Vec, -} - -impl ResolvedCommand for ResolvedInsert { - fn placeholder(&self) -> &Vec { - &self.placeholder - } -} - -impl ResolvedInsert { - pub fn new( - insert_value: Vec<(DatumDesc, ItemValue)>, - placeholder: Vec, - ) -> ResolvedInsert { - Self { - insert_value, - placeholder, - } - } - pub fn insert_value(&self) -> &Vec<(DatumDesc, ItemValue)> { - &self.insert_value - } - - pub fn placeholder(&self) -> &Vec { - &self.placeholder - } -} diff --git a/mudu_runtime/src/resolver/resolved_select.rs b/mudu_runtime/src/resolver/resolved_select.rs deleted file mode 100644 index e9a9910..0000000 --- a/mudu_runtime/src/resolver/resolved_select.rs +++ /dev/null @@ -1,49 +0,0 @@ -use mudu_contract::tuple::datum_desc::DatumDesc; - -use crate::resolver::filter::Filter; - -pub struct ResolvedSelect { - table_name: String, - projection: Vec, - predicate: Vec<(DatumDesc, Filter)>, - predicate_or: Vec>, - placeholder: Vec, -} - -impl ResolvedSelect { - pub fn new( - table_name: String, - projection: Vec, - predicate: Vec<(DatumDesc, Filter)>, - predicate_or: Vec>, - placeholder: Vec, - ) -> Self { - Self { - table_name, - projection, - predicate, - predicate_or, - placeholder, - } - } - - pub fn table_name(&self) -> &String { - &self.table_name - } - - pub fn projection(&self) -> &Vec { - &self.projection - } - - pub fn predicate(&self) -> &Vec<(DatumDesc, Filter)> { - &self.predicate - } - - pub fn predicate_or(&self) -> &Vec> { - &self.predicate_or - } - - pub fn placeholder(&self) -> &Vec { - &self.placeholder - } -} diff --git a/mudu_runtime/src/resolver/resolved_type.rs b/mudu_runtime/src/resolver/resolved_type.rs deleted file mode 100644 index 43958b8..0000000 --- a/mudu_runtime/src/resolver/resolved_type.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::resolver::resolved_select::ResolvedSelect; - -pub enum ResolvedType { - Select(ResolvedSelect), -} diff --git a/mudu_runtime/src/resolver/resolved_update.rs b/mudu_runtime/src/resolver/resolved_update.rs deleted file mode 100644 index 4f61961..0000000 --- a/mudu_runtime/src/resolver/resolved_update.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::resolver::filter::Filter; -use crate::resolver::item_value::ItemValue; -use crate::resolver::resolved_command::ResolvedCommand; -use mudu_contract::tuple::datum_desc::DatumDesc; - -pub struct ResolvedUpdate { - table_name: String, - set_value: Vec<(DatumDesc, ItemValue)>, - predicate: Vec<(DatumDesc, Filter)>, - predicate_or: Vec>, - place_holder: Vec, -} - -impl ResolvedUpdate { - pub fn new( - table_name: String, - set_value: Vec<(DatumDesc, ItemValue)>, - predicate: Vec<(DatumDesc, Filter)>, - predicate_or: Vec>, - place_holder: Vec, - ) -> Self { - Self { - table_name, - set_value, - predicate, - predicate_or, - place_holder, - } - } - - pub fn table_name(&self) -> &String { - &self.table_name - } - - pub fn predicate(&self) -> &Vec<(DatumDesc, Filter)> { - &self.predicate - } - - pub fn predicate_or(&self) -> &Vec> { - &self.predicate_or - } - - pub fn set_value(&self) -> &Vec<(DatumDesc, ItemValue)> { - &self.set_value - } -} - -impl ResolvedCommand for ResolvedUpdate { - fn placeholder(&self) -> &Vec { - &self.place_holder - } -} diff --git a/mudu_runtime/src/resolver/schema_mgr.rs b/mudu_runtime/src/resolver/schema_mgr.rs index dae3bf5..561482f 100644 --- a/mudu_runtime/src/resolver/schema_mgr.rs +++ b/mudu_runtime/src/resolver/schema_mgr.rs @@ -5,18 +5,16 @@ use mudu::m_error; use sql_parser::parser::ddl_parser::DDLParser; use mudu_binding::record::record_def::RecordDef; -use mudu_type::db_type::{DBType, db_type_mgr}; use scc::HashMap as SCCHashMap; use std::collections::HashMap; use std::fs; use std::fs::read_to_string; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; const DDL_SQL_EXTENSION: &str = "sql"; #[derive(Clone)] pub struct SchemaMgr { - map: Arc>>, - db_type: Arc, + tables: Arc>, } lazy_static! { @@ -37,14 +35,13 @@ fn _mgr_remove(app_name: &String) { impl SchemaMgr { pub fn from_sql_text(sql_text: &String) -> RS { - let schema_mgr = SchemaMgr::new_empty()?; let parser = DDLParser::new(); - schema_mgr.load_from_sql_text(sql_text, &parser)?; - Ok(schema_mgr) - } - pub fn db_type(&self) -> &dyn DBType { - self.db_type.as_ref() + let tables = load_table_map_from_sql_text(sql_text, &parser)?; + Ok(Self { + tables: Arc::new(tables), + }) } + pub fn get_mgr(app_name: &String) -> Option { _mgr_get(app_name) } @@ -59,7 +56,7 @@ impl SchemaMgr { pub fn load_from_ddl_path(ddl_path: &String) -> RS { let parser = DDLParser::new(); - let schema_mgr = SchemaMgr::new_empty()?; + let mut tables = HashMap::new(); for entry in fs::read_dir(ddl_path).map_err(|e| { m_error!( EC::MuduError, @@ -85,52 +82,34 @@ impl SchemaMgr { )); } }; - schema_mgr.load_from_sql_text(&str, &parser)?; + tables.extend(load_table_map_from_sql_text(&str, &parser)?); } } } } - Ok(schema_mgr) - } - - pub fn new_empty() -> RS { Ok(Self { - map: Arc::new(Mutex::new(HashMap::new())), - db_type: db_type_mgr()?, + tables: Arc::new(tables), }) } - pub fn insert(&self, key: String, table_def: RecordDef) -> RS { - let mut g = self.map.lock().unwrap(); - if !g.contains_key(&key) { - g.insert(key, table_def); - Ok(true) - } else { - Ok(false) - } - } - pub fn get(&self, key: &String) -> RS> { - let g = self.map.lock().unwrap(); - let opt = g.get(key); - if let Some(def) = opt { - Ok(Some((*def).clone())) - } else { - Ok(None) - } + Ok(self.tables.get(key).cloned()) } pub fn table_names(&self) -> Vec { - let g = self.map.lock().unwrap(); - g.keys().cloned().collect() + self.tables.keys().cloned().collect() } +} - fn load_from_sql_text(&self, sql_text: &String, parser: &DDLParser) -> RS<()> { - let table_def_list = parser.parse(sql_text)?; - for table_def in table_def_list { - self.insert(table_def.table_name().clone(), table_def)?; - } - Ok(()) +fn load_table_map_from_sql_text( + sql_text: &String, + parser: &DDLParser, +) -> RS> { + let table_def_list = parser.parse(sql_text)?; + let mut tables = HashMap::with_capacity(table_def_list.len()); + for table_def in table_def_list { + tables.insert(table_def.table_name().clone(), table_def); } + Ok(tables) } diff --git a/mudu_runtime/src/resolver/sql_resolver.rs b/mudu_runtime/src/resolver/sql_resolver.rs deleted file mode 100644 index 805fe27..0000000 --- a/mudu_runtime/src/resolver/sql_resolver.rs +++ /dev/null @@ -1,253 +0,0 @@ -use crate::resolver::filter::Filter; -use crate::resolver::item_value::ItemValue; -use crate::resolver::resolved_command::ResolvedCommand; -use crate::resolver::resolved_insert::ResolvedInsert; -use crate::resolver::resolved_select::ResolvedSelect; -use crate::resolver::resolved_update::ResolvedUpdate; -use crate::resolver::schema_mgr::SchemaMgr; -use mudu::common::result::RS; -use mudu::common::result_of::rs_option; -use mudu::error::ec::EC; -use mudu::m_error; -use mudu_contract::tuple::datum_desc::DatumDesc; - -use mudu_binding::record::field_def::FieldDef; -use mudu_binding::record::record_def::RecordDef; -use sql_parser::ast::expr_compare::ExprCompare; -use sql_parser::ast::expr_item::{ExprItem, ExprValue}; -use sql_parser::ast::expr_literal::ExprLiteral; -use sql_parser::ast::stmt_insert::StmtInsert; -use sql_parser::ast::stmt_select::StmtSelect; -use sql_parser::ast::stmt_type::StmtCommand; -use sql_parser::ast::stmt_update::{AssignedValue, StmtUpdate}; -use std::sync::Arc; - -/// SQLResolver performs analyzing and checking the semantics of a parsed SQL statement -pub struct SQLResolver { - schema_mgr: SchemaMgr, -} - -impl SQLResolver { - pub fn new(schema_mgr: SchemaMgr) -> Self { - Self { schema_mgr } - } - - pub fn resolve_query(&self, stmt: &StmtSelect) -> RS { - stmt_select_to_resolved(stmt, &self.schema_mgr) - } - - pub fn resolved_command(&self, stmt: &StmtCommand) -> RS> { - let resolved_command: Arc = match stmt { - StmtCommand::Update(update) => { - let update = self.resolve_update(update)?; - Arc::new(update) - } - StmtCommand::Insert(insert) => { - let insert = self.resolve_insert(insert)?; - Arc::new(insert) - } - _ => { - panic!("unsupported command statement {:?}", stmt); - } - }; - Ok(resolved_command) - } - - fn resolve_update(&self, stmt: &StmtUpdate) -> RS { - let table_name = stmt.get_table_reference().clone(); - let table_def = self.get_table(&table_name)?; - let mut vec_set_value = vec![]; - let mut vec_predicate = vec![]; - let mut vec_placeholder = vec![]; - - for assignment in stmt.get_set_values() { - let column_name = assignment.get_column_reference(); - let value = assignment.get_set_value(); - let opt_column_def = table_def.find_column_def_by_name(column_name); - let column_def = rs_option(opt_column_def, "no such column")?; - let desc = DatumDesc::new( - column_def.column_name().clone(), - column_def.dat_type().clone().uni_to()?, - ); - match value { - AssignedValue::Expression(_) => { - // todo set value expression could be > 1 placeholder, to fix it ... - vec_set_value.push((desc.clone(), ItemValue::Placeholder)); - vec_placeholder.push(desc); - } - AssignedValue::Value(v) => match v { - ExprValue::ValueLiteral(v_l) => { - vec_set_value.push((desc, ItemValue::Literal(v_l.dat_type().clone()))); - } - ExprValue::ValuePlaceholder => { - vec_set_value.push((desc.clone(), ItemValue::Placeholder)); - vec_placeholder.push(desc); - } - }, - } - } - - real_where_predicate( - &table_def, - stmt.get_where_predicate(), - &mut vec_predicate, - &mut vec_placeholder, - )?; - let r_update = ResolvedUpdate::new( - table_name, - vec_set_value, - vec_predicate, - vec![], - vec_placeholder, - ); - Ok(r_update) - } - - fn resolve_insert(&self, stmt: &StmtInsert) -> RS { - let table_def = self.get_table(stmt.table_name())?; - - let mut vec_column_def = vec![]; - let value_columns = if stmt.columns().is_empty() { - table_def.table_columns() - } else { - for column_name in stmt.columns() { - let col = Self::get_column(&table_def, column_name)?; - vec_column_def.push(col.clone()); - } - &vec_column_def - }; - - for value in stmt.values_list().iter() { - if value_columns.len() != value.len() { - return Err(m_error!( - EC::ParseErr, - format!("column and value size are not equal {:?}", stmt) - )); - } - } - if stmt.values_list().len() != 1 { - return Err(m_error!(EC::MuduError, "only support 1 row insert")); - } - let mut insert_values = vec![]; - let mut placeholder = vec![]; - let row = &stmt.values_list()[0]; - for (i, v) in row.iter().enumerate() { - let c_def = &value_columns[i]; - let desc = DatumDesc::new( - c_def.column_name().clone(), - c_def.dat_type().clone().uni_to()?, - ); - - let item_value = match v { - ExprValue::ValueLiteral(l) => ItemValue::Literal(l.dat_type().clone()), - ExprValue::ValuePlaceholder => { - placeholder.push(desc.clone()); - ItemValue::Placeholder - } - }; - insert_values.push((desc, item_value)); - } - Ok(ResolvedInsert::new(insert_values, placeholder)) - } - - fn get_column<'a>(table_def: &'a RecordDef, column_name: &String) -> RS<&'a FieldDef> { - let opt_column_def = table_def.find_column_def_by_name(column_name); - let column_def = rs_option( - opt_column_def, - &format!("no such column named {}", column_name), - )?; - Ok(column_def) - } - - fn get_table(&self, table_name: &String) -> RS { - let opt_table_def = self.schema_mgr.get(table_name)?; - let table_def = rs_option( - opt_table_def, - &format!("no such table named {}", table_name), - )?; - Ok(table_def) - } -} - -fn build_data_desc_for_name(column_name: &str, table_def: &RecordDef) -> RS { - let opt = table_def.find_column_def_by_name(column_name); - let column_def = rs_option(opt, &format!("no such column {}", column_name))?; - let datum_desc = DatumDesc::new( - column_name.to_string(), - column_def.dat_type().clone().uni_to()?, - ); - Ok(datum_desc) -} - -fn real_where_predicate( - table_def: &RecordDef, - expr_compare_list: &Vec, - vec_predicate: &mut Vec<(DatumDesc, Filter)>, - vec_placeholder: &mut Vec, -) -> RS<()> { - for predicate in expr_compare_list { - let right = predicate.right(); - let left = predicate.left(); - match (left, right) { - (ExprItem::ItemName(expr_name), ExprItem::ItemValue(value)) => match value { - ExprValue::ValueLiteral(literal) => { - let datum_desc = build_data_desc_for_name(expr_name.name(), &table_def)?; - let literal_value = match literal { - ExprLiteral::DatumLiteral(typed) => typed.clone(), - }; - let filter = Filter::new(*predicate.op(), ItemValue::Literal(literal_value)); - vec_predicate.push((datum_desc, filter)); - } - ExprValue::ValuePlaceholder => { - let datum_desc = build_data_desc_for_name(expr_name.name(), &table_def)?; - let filter = Filter::new(*predicate.op(), ItemValue::Placeholder); - vec_predicate.push((datum_desc.clone(), filter)); - vec_placeholder.push(datum_desc); - } - }, - (_, _) => { - return Err(m_error!( - EC::ParseErr, - format!( - "\ -In where filter, the left must be name, \ -the right must be a placeholder or literal value,\ -but got {:?} {:?}\ - ", - left, right - ) - )); - } - } - } - Ok(()) -} - -fn stmt_select_to_resolved(stmt: &StmtSelect, schema_mgr: &SchemaMgr) -> RS { - let table_name = stmt.get_table_reference(); - let opt = schema_mgr.get(table_name)?; - let mut vec_projection = vec![]; - - let table_def = rs_option(opt, &format!("no such table {}", table_name))?; - for term in stmt.get_select_term_list() { - let datum_desc = build_data_desc_for_name(term.field().name(), &table_def)?; - vec_projection.push(datum_desc); - } - let mut vec_predicate = vec![]; - let mut vec_placeholder = vec![]; - real_where_predicate( - &table_def, - stmt.get_where_predicate(), - &mut vec_predicate, - &mut vec_placeholder, - )?; - - let rs = ResolvedSelect::new( - table_name.clone(), - vec_projection, - vec_predicate, - vec![], - vec_placeholder, - ); - Ok(rs) -} diff --git a/mudu_runtime/src/service/app_inst_impl.rs b/mudu_runtime/src/service/app_inst_impl.rs index 1d6315e..33406bc 100644 --- a/mudu_runtime/src/service/app_inst_impl.rs +++ b/mudu_runtime/src/service/app_inst_impl.rs @@ -1,3 +1,4 @@ +use crate::backend::mududb_cfg::ServerMode; use crate::db_connector::DBConnector; use crate::procedure::procedure::Procedure; use crate::resolver::schema_mgr::SchemaMgr; @@ -31,6 +32,7 @@ pub struct AppInstImpl { struct AppInstImplInner { package_cfg: AppInfo, enable_async: bool, + server_mode: ServerMode, db_path: String, schema_mgr: SchemaMgr, modules: HashMap, @@ -45,6 +47,7 @@ impl AppInstImpl { vec_modules: Vec<(String, PackageModule)>, component_target: ComponentTarget, enable_async: bool, + server_mode: ServerMode, ) -> RS { Ok(Self { inner: Arc::new( @@ -54,6 +57,7 @@ impl AppInstImpl { vec_modules, component_target, enable_async, + server_mode, ) .await?, ), @@ -92,6 +96,7 @@ impl AppInstImplInner { vec_modules: Vec<(String, PackageModule)>, component_target: ComponentTarget, enable_async: bool, + server_mode: ServerMode, ) -> RS { let modules = HashMap::new(); let app_cfg = &package.package_cfg; @@ -103,10 +108,19 @@ impl AppInstImplInner { } SchemaMgr::add_mgr(app_cfg.name.clone(), schema_mgr.clone()); let sql_text = ddl_sql.to_string() + init_sql.as_str(); - initdb(db_path, &app_cfg.name, &sql_text, &schema_mgr, enable_async).await?; + initdb( + db_path, + &app_cfg.name, + &sql_text, + &schema_mgr, + enable_async, + server_mode, + ) + .await?; Ok(Self { package_cfg: app_cfg.clone(), enable_async, + server_mode, db_path: db_path.clone(), schema_mgr, modules, @@ -202,7 +216,13 @@ impl AppInstImplInner { } pub async fn create_conn(&self, task_id: u128) -> RS<()> { - let db_conn = new_conn(&self.db_path, &self.package_cfg.name, self.enable_async).await?; + let db_conn = new_conn( + &self.db_path, + &self.package_cfg.name, + self.enable_async, + self.server_mode, + ) + .await?; self._conn.insert_sync(task_id, db_conn).map_err(|_e| { m_error!( EC::ExistingSuchElement, @@ -259,8 +279,15 @@ impl AppInstImplInner { } } -async fn new_conn(db_path: &String, app_name: &String, enable_async: bool) -> RS { - let db_type = if enable_async { +async fn new_conn( + db_path: &String, + app_name: &String, + enable_async: bool, + server_mode: ServerMode, +) -> RS { + let db_type = if server_mode == ServerMode::IOUring { + "MuduDB".to_string() + } else if enable_async { "LibSQLAsync".to_string() } else { "LibSQL".to_string() @@ -276,14 +303,18 @@ async fn initdb( sql: &String, schema_mgr: &SchemaMgr, enable_async: bool, + server_mode: ServerMode, ) -> RS<()> { let init_db_lock = PathBuf::from(&db_path).join(format!("{}.lock", app_name)); - if init_db_lock.exists() - && is_schema_initialized(db_path, app_name, schema_mgr, enable_async).await? - { - return Ok(()); + if init_db_lock.exists() { + if server_mode == ServerMode::IOUring { + return Ok(()); + } + if is_schema_initialized(db_path, app_name, schema_mgr, enable_async, server_mode).await? { + return Ok(()); + } } - let conn = new_conn(db_path, app_name, enable_async).await?; + let conn = new_conn(db_path, app_name, enable_async, server_mode).await?; conn.execute_silent(sql.clone()).await?; File::create(&init_db_lock).map_err(|e| { m_error!( @@ -300,8 +331,9 @@ async fn is_schema_initialized( app_name: &String, schema_mgr: &SchemaMgr, enable_async: bool, + server_mode: ServerMode, ) -> RS { - let conn = new_conn(db_path, app_name, enable_async).await?; + let conn = new_conn(db_path, app_name, enable_async, server_mode).await?; for table_name in schema_mgr.table_names() { let verify_sql = format!("SELECT 1 FROM {} LIMIT 1;", table_name); if conn.execute_silent(verify_sql).await.is_err() { diff --git a/mudu_runtime/src/service/runtime_opt.rs b/mudu_runtime/src/service/runtime_opt.rs index 8385a97..5700b20 100644 --- a/mudu_runtime/src/service/runtime_opt.rs +++ b/mudu_runtime/src/service/runtime_opt.rs @@ -1,3 +1,4 @@ +use crate::backend::mududb_cfg::ServerMode; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Default)] @@ -13,6 +14,7 @@ pub struct RuntimeOpt { #[serde(default)] pub component_target: ComponentTarget, pub enable_async: bool, + pub sever_mode: ServerMode, } impl RuntimeOpt { @@ -26,6 +28,7 @@ impl Default for RuntimeOpt { Self { component_target: ComponentTarget::P2, enable_async: false, + sever_mode: Default::default(), } } } diff --git a/mudu_runtime/src/service/runtime_simple.rs b/mudu_runtime/src/service/runtime_simple.rs index 3f87a8a..f267ac3 100644 --- a/mudu_runtime/src/service/runtime_simple.rs +++ b/mudu_runtime/src/service/runtime_simple.rs @@ -140,6 +140,7 @@ impl RuntimeSimple { modules, self.rt_opt.component_target(), self.rt_opt.enable_async, + self.rt_opt.sever_mode, ) .await?; let mpk_name = app_instance.name().clone(); diff --git a/mudu_runtime/src/service/runtime_simple_test.rs b/mudu_runtime/src/service/runtime_simple_test.rs index c5259bf..8aef19d 100644 --- a/mudu_runtime/src/service/runtime_simple_test.rs +++ b/mudu_runtime/src/service/runtime_simple_test.rs @@ -69,6 +69,7 @@ mod tests { RuntimeOpt { component_target: crate::service::runtime_opt::ComponentTarget::P2, enable_async, + sever_mode: Default::default(), }, ) .await?; diff --git a/mudu_runtime/src/service/service_impl.rs b/mudu_runtime/src/service/service_impl.rs index c3b9a20..2335d68 100644 --- a/mudu_runtime/src/service/service_impl.rs +++ b/mudu_runtime/src/service/service_impl.rs @@ -3,7 +3,7 @@ use mudu::common::result::RS; use mudu::error::ec::EC; use mudu::m_error; use mudu_utils::sync::async_task::TaskWrapper; -use tracing::info; +use tracing::debug; pub struct ServiceImpl { tasks: scc::Queue, @@ -58,7 +58,7 @@ impl ServiceTrait for ServiceImpl { Ok(result) } })?; - info!("task join result: {:?}", r); + debug!("task join result: {:?}", r); Ok(()) } } diff --git a/mudu_runtime/src/service/wt_runtime_component_test.rs b/mudu_runtime/src/service/wt_runtime_component_test.rs index bdcf87f..e12aff2 100644 --- a/mudu_runtime/src/service/wt_runtime_component_test.rs +++ b/mudu_runtime/src/service/wt_runtime_component_test.rs @@ -39,6 +39,7 @@ mod tests { let mut runtime = WTRuntimeComponent::build(&RuntimeOpt { component_target: ComponentTarget::P3, enable_async: false, + sever_mode: Default::default(), }) .unwrap(); diff --git a/mudu_sys/Cargo.toml b/mudu_sys/Cargo.toml index 1b475ea..d5892af 100644 --- a/mudu_sys/Cargo.toml +++ b/mudu_sys/Cargo.toml @@ -7,9 +7,11 @@ edition = "2021" mudu = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } +async-trait = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] libc = { workspace = true } socket2 = "0.5.8" -async-trait = { workspace = true } tokio = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] diff --git a/mudu_sys/src/env.rs b/mudu_sys/src/env.rs index bab9494..41e95c4 100644 --- a/mudu_sys/src/env.rs +++ b/mudu_sys/src/env.rs @@ -1,11 +1,14 @@ use crate::api::env::SysEnv; +#[cfg(target_os = "linux")] use crate::linux::env::LinuxSysEnv; +#[cfg(not(target_os = "linux"))] +use crate::portable::env::PortableSysEnv; use std::sync::{Arc, OnceLock, RwLock}; static DEFAULT_ENV: OnceLock>> = OnceLock::new(); fn default_env_cell() -> &'static RwLock> { - DEFAULT_ENV.get_or_init(|| RwLock::new(Arc::new(LinuxSysEnv::new()))) + DEFAULT_ENV.get_or_init(|| RwLock::new(Arc::new(default_sys_env()))) } pub fn default_env() -> Arc { @@ -23,5 +26,15 @@ pub fn set_default_env(env: Arc) { } pub fn reset_default_env() { - set_default_env(Arc::new(LinuxSysEnv::new())); + set_default_env(Arc::new(default_sys_env())); +} + +#[cfg(target_os = "linux")] +fn default_sys_env() -> impl SysEnv { + LinuxSysEnv::new() +} + +#[cfg(not(target_os = "linux"))] +fn default_sys_env() -> impl SysEnv { + PortableSysEnv::new() } diff --git a/mudu_sys/src/fd.rs b/mudu_sys/src/fd.rs index fdb43af..e043911 100644 --- a/mudu_sys/src/fd.rs +++ b/mudu_sys/src/fd.rs @@ -2,4 +2,4 @@ pub type RawFd = std::os::fd::RawFd; #[cfg(not(unix))] -pub type RawFd = libc::c_int; +pub type RawFd = i32; diff --git a/mudu_sys/src/lib.rs b/mudu_sys/src/lib.rs index 9617156..c2acd07 100644 --- a/mudu_sys/src/lib.rs +++ b/mudu_sys/src/lib.rs @@ -2,8 +2,11 @@ pub mod api; pub mod env; pub mod fd; pub mod fs; +#[cfg(target_os = "linux")] pub mod linux; pub mod net; +#[cfg(not(target_os = "linux"))] +mod portable; pub mod sync; pub mod task; #[cfg(target_os = "linux")] diff --git a/mudu_sys/src/portable/env.rs b/mudu_sys/src/portable/env.rs new file mode 100644 index 0000000..a3c5c25 --- /dev/null +++ b/mudu_sys/src/portable/env.rs @@ -0,0 +1,252 @@ +use crate::api::env::SysEnv; +use crate::api::fs::SysFs; +use crate::api::net::SysNet; +use crate::api::random::SysRandom; +use crate::api::sync::SysSync; +use crate::api::task::SysTask; +use crate::api::time::SysTime; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use mudu::common::result::RS; +use mudu::error::ec::EC; +use mudu::m_error; +use std::fs::File; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant, SystemTime}; +use uuid::Uuid; + +pub struct PortableSysEnv { + time: PortableTime, + random: PortableRandom, + fs: PortableFs, + net: UnsupportedNet, + task: PortableTask, + sync: UnsupportedSync, +} + +impl PortableSysEnv { + pub fn new() -> Self { + Self { + time: PortableTime, + random: PortableRandom, + fs: PortableFs, + net: UnsupportedNet, + task: PortableTask, + sync: UnsupportedSync, + } + } +} + +impl SysEnv for PortableSysEnv { + fn time(&self) -> &dyn SysTime { + &self.time + } + + fn random(&self) -> &dyn SysRandom { + &self.random + } + + fn fs(&self) -> &dyn SysFs { + &self.fs + } + + fn net(&self) -> &dyn SysNet { + &self.net + } + + fn task(&self) -> &dyn SysTask { + &self.task + } + + fn sync(&self) -> &dyn SysSync { + &self.sync + } +} + +struct PortableTime; + +impl SysTime for PortableTime { + fn instant_now(&self) -> Instant { + Instant::now() + } + + fn system_time_now(&self) -> SystemTime { + SystemTime::now() + } + + fn utc_now(&self) -> DateTime { + Utc::now() + } +} + +struct PortableRandom; + +impl SysRandom for PortableRandom { + fn uuid_v4(&self) -> Uuid { + Uuid::new_v4() + } +} + +struct PortableFs; + +impl SysFs for PortableFs { + fn open(&self, path: &Path, flags: i32, _mode: u32) -> RS { + let mut options = std::fs::OpenOptions::new(); + let read = (flags & libc::O_RDWR) != 0 || (flags & libc::O_WRONLY) == 0; + let write = (flags & libc::O_RDWR) != 0 || (flags & libc::O_WRONLY) != 0; + options.read(read); + options.write(write); + options.create((flags & libc::O_CREAT) != 0); + options.truncate((flags & libc::O_TRUNC) != 0); + options.append((flags & libc::O_APPEND) != 0); + options + .open(path) + .map_err(|e| m_error!(EC::IOErr, "open file error", e)) + } + + fn read_exact_at(&self, file: &File, len: usize, offset: u64) -> RS> { + let mut cloned = file + .try_clone() + .map_err(|e| m_error!(EC::IOErr, "clone file for read_exact_at error", e))?; + cloned + .seek(SeekFrom::Start(offset)) + .map_err(|e| m_error!(EC::IOErr, "seek for read_exact_at error", e))?; + let mut buf = vec![0u8; len]; + cloned + .read_exact(&mut buf) + .map_err(|e| m_error!(EC::IOErr, "read_exact_at error", e))?; + Ok(buf) + } + + fn write_all_at(&self, file: &File, payload: &[u8], offset: u64) -> RS<()> { + let mut cloned = file + .try_clone() + .map_err(|e| m_error!(EC::IOErr, "clone file for write_all_at error", e))?; + cloned + .seek(SeekFrom::Start(offset)) + .map_err(|e| m_error!(EC::IOErr, "seek for write_all_at error", e))?; + cloned + .write_all(payload) + .map_err(|e| m_error!(EC::IOErr, "write_all_at error", e))?; + Ok(()) + } + + fn fsync(&self, file: &File) -> RS<()> { + file.sync_all() + .map_err(|e| m_error!(EC::IOErr, "fsync error", e)) + } + + fn close(&self, file: File) -> RS<()> { + drop(file); + Ok(()) + } + + fn create_dir_all(&self, path: &Path) -> RS<()> { + std::fs::create_dir_all(path).map_err(|e| { + m_error!( + EC::IOErr, + format!("create_dir_all {} error", path.display()), + e + ) + }) + } + + fn read_dir(&self, path: &Path) -> RS> { + let mut entries = Vec::new(); + for entry in std::fs::read_dir(path) + .map_err(|e| m_error!(EC::IOErr, format!("read_dir {} error", path.display()), e))? + { + let entry = entry.map_err(|e| m_error!(EC::IOErr, "read_dir entry error", e))?; + entries.push(entry.path()); + } + Ok(entries) + } + + fn metadata_len(&self, path: &Path) -> RS { + Ok(std::fs::metadata(path) + .map_err(|e| m_error!(EC::IOErr, format!("metadata {} error", path.display()), e))? + .len()) + } + + fn read_all(&self, path: &Path) -> RS> { + std::fs::read(path) + .map_err(|e| m_error!(EC::IOErr, format!("read_all {} error", path.display()), e)) + } + + fn remove_file_if_exists(&self, path: &Path) -> RS<()> { + match std::fs::remove_file(path) { + Ok(()) => Ok(()), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(err) => Err(m_error!( + EC::IOErr, + format!("remove_file_if_exists {} error", path.display()), + err + )), + } + } +} + +struct UnsupportedNet; + +impl SysNet for UnsupportedNet { + fn create_tcp_listener_fd(&self, _listen_addr: std::net::SocketAddr, _backlog: i32) -> RS { + Err(m_error!( + EC::NotImplemented, + "network operations are not supported on this target" + )) + } + + fn set_tcp_nodelay(&self, _fd: i32) -> RS<()> { + Err(m_error!( + EC::NotImplemented, + "network operations are not supported on this target" + )) + } +} + +struct UnsupportedSync; + +impl SysSync for UnsupportedSync { + fn eventfd(&self) -> RS { + Err(m_error!( + EC::NotImplemented, + "eventfd is not supported on this target" + )) + } + + fn notify_eventfd(&self, _fd: i32) -> RS<()> { + Err(m_error!( + EC::NotImplemented, + "eventfd is not supported on this target" + )) + } + + fn read_eventfd(&self, _fd: i32) -> RS { + Err(m_error!( + EC::NotImplemented, + "eventfd is not supported on this target" + )) + } + + fn close_fd(&self, _fd: i32) -> RS<()> { + Err(m_error!( + EC::NotImplemented, + "eventfd is not supported on this target" + )) + } +} + +struct PortableTask; + +#[async_trait] +impl SysTask for PortableTask { + async fn sleep(&self, dur: Duration) -> RS<()> { + std::thread::sleep(dur); + Ok(()) + } + + fn sleep_blocking(&self, dur: Duration) { + std::thread::sleep(dur); + } +} diff --git a/mudu_sys/src/portable/mod.rs b/mudu_sys/src/portable/mod.rs new file mode 100644 index 0000000..3d7924f --- /dev/null +++ b/mudu_sys/src/portable/mod.rs @@ -0,0 +1 @@ +pub mod env; diff --git a/mudu_sys/src/task.rs b/mudu_sys/src/task.rs index e1d7806..fc681db 100644 --- a/mudu_sys/src/task.rs +++ b/mudu_sys/src/task.rs @@ -2,6 +2,7 @@ use crate::env::default_env; use mudu::common::result::RS; use mudu::error::ec::EC; use mudu::m_error; +#[cfg(not(target_arch = "wasm32"))] use std::future::Future; use std::thread; use std::time::Duration; @@ -33,6 +34,7 @@ where .map_err(|e| m_error!(EC::ThreadErr, "spawn thread error", e)) } +#[cfg(not(target_arch = "wasm32"))] pub fn spawn_tokio(fut: F) -> tokio::task::JoinHandle where F: Future + Send + 'static, diff --git a/mudu_type/src/dat_value.rs b/mudu_type/src/dat_value.rs index 5faa505..73ca85f 100644 --- a/mudu_type/src/dat_value.rs +++ b/mudu_type/src/dat_value.rs @@ -7,12 +7,13 @@ use mudu::common::result::RS; use mudu::error::ec::EC; use mudu::m_error; use paste::paste; +use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::hint; /// A memory-efficient representation of data that can hold various primitive types /// or complex types (arrays, records) in a unified enum container. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct DatValue { inner: ValueKind, } @@ -29,6 +30,7 @@ impl AsRef for DatValue { /// Internal memory representation supporting various data types /// Uses Box for time_series allocation of complex types to avoid large enum variants +#[derive(Clone, Debug, Serialize, Deserialize)] enum ValueKind { F32(f32), F64(f64), @@ -69,31 +71,6 @@ macro_rules! impl_dat_value_methods { } } } - // Automatically generates debug arms for all enum variant - impl std::fmt::Debug for ValueKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - $( - ValueKind::$variant_upper(value) => { - write!(f, "{}({:?})", stringify!($variant_upper), value) - } - )+ - } - } - } - - // Automatically generates clone arms for all enum variant - impl Clone for ValueKind { - fn clone(&self) -> Self { - match self { - $( - ValueKind::$variant_upper(value) => { - Self::$variant_upper(value.clone()) - } - )+ - } - } - } }; // Handling for non-boxed types @@ -257,6 +234,7 @@ impl DatumDyn for DatValue { #[cfg(test)] mod tests { use crate::dat_value::DatValue; + use serde_json::json; #[test] fn test() { @@ -272,4 +250,33 @@ mod tests { assert_eq!(mem.expect_i32(), &i); assert!(mem.as_string().is_none()); } + + #[test] + fn serde_roundtrip_json() { + let value = DatValue::from_record(vec![ + DatValue::from_i32(7), + DatValue::from_string("hello".to_string()), + DatValue::from_array(vec![DatValue::from_i64(9), DatValue::from_binary(vec![1, 2, 3])]), + ]); + + let json_value = serde_json::to_value(&value).unwrap(); + assert_eq!( + json_value, + json!({ + "inner": { + "Record": [ + {"inner": {"I32": 7}}, + {"inner": {"String": "hello"}}, + {"inner": {"Array": [ + {"inner": {"I64": 9}}, + {"inner": {"Binary": [1, 2, 3]}} + ]}} + ] + } + }) + ); + + let from_json: DatValue = serde_json::from_value(json_value).unwrap(); + assert_eq!(from_json.expect_record().len(), 3); + } } diff --git a/mudu_type/src/dt_impl/compare_test.rs b/mudu_type/src/dt_impl/compare_test.rs new file mode 100644 index 0000000..78ae711 --- /dev/null +++ b/mudu_type/src/dt_impl/compare_test.rs @@ -0,0 +1,109 @@ +use crate::dat_type_id::DatTypeID; +use crate::dat_value::DatValue; +use arbitrary::Unstructured; +use std::cmp::Ordering; +use std::collections::hash_map::DefaultHasher; +use std::hash::Hasher; + +const SEED_COUNT: u64 = 32; +const SEED_BYTES_LEN: usize = 256; + +fn comparable_type_ids() -> &'static [DatTypeID] { + &[ + DatTypeID::I32, + DatTypeID::I64, + DatTypeID::String, + DatTypeID::U128, + DatTypeID::I128, + ] +} + +fn seed_bytes(seed: u64, len: usize) -> Vec { + let mut state = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15); + let mut out = Vec::with_capacity(len); + for _ in 0..len { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + out.push((state & 0xff) as u8); + } + out +} + +fn value_hash(id: DatTypeID, value: &DatValue) -> u64 { + let mut hasher = DefaultHasher::new(); + id.fn_hash().unwrap()(value, &mut hasher).unwrap(); + hasher.finish() +} + +#[test] +fn compare_functions_are_reflexive_and_hash_stable() { + for &id in comparable_type_ids() { + let order = id.fn_order().unwrap(); + let equal = id.fn_equal().unwrap(); + + for seed in 0..SEED_COUNT { + let bytes = seed_bytes(seed ^ ((id.to_u32() as u64) << 8), SEED_BYTES_LEN); + let mut u = Unstructured::new(&bytes); + let dt = id.fn_arb_param()(&mut u).unwrap(); + let value = match id.fn_arb_internal()(&mut u, &dt) { + Ok(value) => value, + Err(arbitrary::Error::NotEnoughData) => continue, + Err(err) => panic!("arb value failed for {:?}: {:?}", id, err), + }; + + assert!(equal(&value, &value).unwrap(), "equal is not reflexive for {:?}", id); + assert_eq!(order(&value, &value).unwrap(), Ordering::Equal); + + let h1 = value_hash(id, &value); + let h2 = value_hash(id, &value); + assert_eq!(h1, h2, "hash is unstable for {:?}", id); + } + } +} + +#[test] +fn compare_functions_are_symmetric_and_consistent() { + for &id in comparable_type_ids() { + let order = id.fn_order().unwrap(); + let equal = id.fn_equal().unwrap(); + + for seed in 0..SEED_COUNT { + let left_bytes = seed_bytes(seed ^ ((id.to_u32() as u64) << 16), SEED_BYTES_LEN); + let right_bytes = seed_bytes((seed + 1) ^ ((id.to_u32() as u64) << 24), SEED_BYTES_LEN); + let mut left_u = Unstructured::new(&left_bytes); + let mut right_u = Unstructured::new(&right_bytes); + + let left_dt = id.fn_arb_param()(&mut left_u).unwrap(); + let right_dt = id.fn_arb_param()(&mut right_u).unwrap(); + let left = match id.fn_arb_internal()(&mut left_u, &left_dt) { + Ok(value) => value, + Err(arbitrary::Error::NotEnoughData) => continue, + Err(err) => panic!("left arb value failed for {:?}: {:?}", id, err), + }; + let right = match id.fn_arb_internal()(&mut right_u, &right_dt) { + Ok(value) => value, + Err(arbitrary::Error::NotEnoughData) => continue, + Err(err) => panic!("right arb value failed for {:?}: {:?}", id, err), + }; + + let left_right_equal = equal(&left, &right).unwrap(); + let right_left_equal = equal(&right, &left).unwrap(); + assert_eq!(left_right_equal, right_left_equal, "equal symmetry failed for {:?}", id); + + let left_right_order = order(&left, &right).unwrap(); + let right_left_order = order(&right, &left).unwrap(); + assert_eq!( + left_right_order, + right_left_order.reverse(), + "order symmetry failed for {:?}", + id + ); + + if left_right_equal { + assert_eq!(left_right_order, Ordering::Equal); + assert_eq!(value_hash(id, &left), value_hash(id, &right)); + } + } + } +} diff --git a/mudu_type/src/dt_impl/error_test.rs b/mudu_type/src/dt_impl/error_test.rs new file mode 100644 index 0000000..92403aa --- /dev/null +++ b/mudu_type/src/dt_impl/error_test.rs @@ -0,0 +1,188 @@ +use crate::dat_type::DatType; +use crate::dat_type_id::DatTypeID; +use crate::dat_value::DatValue; +use crate::dt_impl::dt_create::{create_array_type, create_object_type, create_string_type}; +use crate::type_error::{TyEC, TyErr}; +use mudu::utils::json::JsonValue; + +fn assert_ty_ec(err: TyErr, ec: TyEC) { + assert_eq!( + std::mem::discriminant(&err.ec()), + std::mem::discriminant(&ec) + ); +} + +#[test] +fn invalid_textual_input_paths_return_type_convert_failed() { + let cases = vec![ + (DatTypeID::I32, DatType::new_no_param(DatTypeID::I32), "\"bad\""), + (DatTypeID::I64, DatType::new_no_param(DatTypeID::I64), "\"bad\""), + (DatTypeID::F32, DatType::new_no_param(DatTypeID::F32), "\"bad\""), + (DatTypeID::F64, DatType::new_no_param(DatTypeID::F64), "\"bad\""), + (DatTypeID::String, create_string_type(Some(8)), "not-json"), + (DatTypeID::U128, DatType::new_no_param(DatTypeID::U128), "\"not-a-u128\""), + (DatTypeID::I128, DatType::new_no_param(DatTypeID::I128), "\"not-an-i128\""), + (DatTypeID::Binary, DatType::new_no_param(DatTypeID::Binary), "{\"oops\":1}"), + ( + DatTypeID::Array, + create_array_type(DatType::new_no_param(DatTypeID::I32)), + "{\"oops\":1}", + ), + ( + DatTypeID::Record, + create_object_type( + "user".to_string(), + vec![("name".to_string(), create_string_type(Some(16)))], + ), + "[1,2,3]", + ), + ]; + + for (id, dt, textual) in cases { + let err = id.fn_input()(textual, &dt).unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + } +} + +#[test] +fn textual_input_rejects_json_with_wrong_shape() { + let cases = vec![ + (DatTypeID::I32, DatType::new_no_param(DatTypeID::I32), "{\"abc\""), + (DatTypeID::I64, DatType::new_no_param(DatTypeID::I64), "{\"abc\""), + (DatTypeID::F32, DatType::new_no_param(DatTypeID::F32), "{\"abc\""), + (DatTypeID::F64, DatType::new_no_param(DatTypeID::F64), "{\"abc\""), + (DatTypeID::String, create_string_type(Some(8)), "{ 123"), + (DatTypeID::U128, DatType::new_no_param(DatTypeID::U128), "{true"), + (DatTypeID::I128, DatType::new_no_param(DatTypeID::I128), "{ false"), + (DatTypeID::Binary, DatType::new_no_param(DatTypeID::Binary), "{ [\"bad\"]"), + ( + DatTypeID::Array, + create_array_type(DatType::new_no_param(DatTypeID::I32)), + "{[\"bad\"]", + ), + ( + DatTypeID::Record, + create_object_type( + "user".to_string(), + vec![("name".to_string(), create_string_type(Some(16)))], + ), + "{\"name\":123", + ), + ]; + + for (id, dt, textual) in cases { + let err = id.fn_input()(textual, &dt).unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + } +} + +#[test] +fn string_error_paths_return_expected_error_codes() { + let dt = create_string_type(Some(8)); + + let err = DatTypeID::String.fn_input()("not-json", &dt).unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + + let err = DatTypeID::String + .fn_input_json()(&JsonValue::Bool(true), &dt) + .unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + + let err = DatTypeID::String.fn_recv()(&[0, 0], &dt).unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + + let value = DatValue::from_string("abcdef".to_string()); + let err = DatTypeID::String + .fn_send_to()(&value, &dt, &mut [0u8; 4]) + .unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); +} + +#[test] +fn binary_error_paths_return_expected_error_codes() { + let dt = DatType::new_no_param(DatTypeID::Binary); + + let err = DatTypeID::Binary + .fn_input_json()(&JsonValue::String("oops".to_string()), &dt) + .unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + + let err = DatTypeID::Binary + .fn_input_json()( + &JsonValue::Array(vec![JsonValue::String("bad".to_string())]), + &dt, + ) + .unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + + let err = DatTypeID::Binary.fn_recv()(&[0, 0, 0], &dt).unwrap_err(); + assert_ty_ec(err, TyEC::InsufficientSpace); + + let value = DatValue::from_binary(vec![1, 2, 3]); + let err = DatTypeID::Binary + .fn_send_to()(&value, &dt, &mut [0u8; 2]) + .unwrap_err(); + assert_ty_ec(err, TyEC::InsufficientSpace); +} + +#[test] +fn array_error_paths_return_expected_error_codes() { + let dt = create_array_type(DatType::new_no_param(DatTypeID::I32)); + + let err = DatTypeID::Array + .fn_input_json()(&JsonValue::String("oops".to_string()), &dt) + .unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + + let err = DatTypeID::Array + .fn_input_json()( + &JsonValue::Array(vec![JsonValue::String("bad".to_string())]), + &dt, + ) + .unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + + let err = DatTypeID::Array.fn_recv()(&[0, 0, 0, 0], &dt).unwrap_err(); + assert_ty_ec(err, TyEC::InsufficientSpace); +} + +#[test] +fn object_error_paths_return_expected_error_codes() { + let dt = create_object_type( + "user".to_string(), + vec![ + ("name".to_string(), create_string_type(Some(16))), + ("age".to_string(), DatType::new_no_param(DatTypeID::I32)), + ], + ); + + let err = DatTypeID::Record + .fn_input_json()( + &JsonValue::Object( + [("name".to_string(), JsonValue::String("neo".to_string()))] + .into_iter() + .collect(), + ), + &dt, + ) + .unwrap_err(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + + let err = DatTypeID::Record + .fn_output_json()(&DatValue::from_record(vec![DatValue::from_string("neo".to_string())]), &dt) + .err() + .unwrap(); + assert_ty_ec(err, TyEC::TypeConvertFailed); + + let value = DatValue::from_record(vec![ + DatValue::from_string("neo".to_string()), + DatValue::from_i32(7), + ]); + let err = DatTypeID::Record + .fn_send_to()(&value, &dt, &mut [0u8; 4]) + .unwrap_err(); + assert_ty_ec(err, TyEC::InsufficientSpace); + + let err = DatTypeID::Record.fn_recv()(&[0, 0, 0, 0], &dt).unwrap_err(); + assert_ty_ec(err, TyEC::InsufficientSpace); +} diff --git a/mudu_type/src/dt_impl/fn_array_arb.rs b/mudu_type/src/dt_impl/fn_array_arb.rs index 59a1fdf..de9d524 100644 --- a/mudu_type/src/dt_impl/fn_array_arb.rs +++ b/mudu_type/src/dt_impl/fn_array_arb.rs @@ -6,6 +6,17 @@ use crate::dt_fn_arbitrary::FnArbitrary; use crate::dtp_array::DTPArray; use arbitrary::{Arbitrary, Unstructured}; +const ARRAY_INNER_TYPE_IDS: [DatTypeID; 8] = [ + DatTypeID::I32, + DatTypeID::I64, + DatTypeID::F32, + DatTypeID::F64, + DatTypeID::String, + DatTypeID::U128, + DatTypeID::I128, + DatTypeID::Binary, +]; + pub fn fn_array_arb_object( u: &mut Unstructured, dat_type: &DatType, @@ -31,9 +42,14 @@ pub fn fn_array_arb_printable( } pub fn fn_array_arb_dt_param(u: &mut Unstructured) -> arbitrary::Result { - let n = u8::arbitrary(u)? as u32; - let dat_type_id = DatTypeID::from_u32(n); - let param = DTPArray::new(DatType::default_for(dat_type_id)); + let n = (u8::arbitrary(u)? as usize) % ARRAY_INNER_TYPE_IDS.len(); + let dat_type_id = ARRAY_INNER_TYPE_IDS[n]; + let inner_type = if dat_type_id.has_param() { + dat_type_id.fn_arb_param()(u)? + } else { + DatType::default_for(dat_type_id) + }; + let param = DTPArray::new(inner_type); let dat_type = DatType::from_array(param); Ok(dat_type) } diff --git a/mudu_type/src/dt_impl/fn_binary.rs b/mudu_type/src/dt_impl/fn_binary.rs index cab3575..952b57b 100644 --- a/mudu_type/src/dt_impl/fn_binary.rs +++ b/mudu_type/src/dt_impl/fn_binary.rs @@ -131,11 +131,18 @@ pub fn fn_binary_send(dat_value: &DatValue, dat_type: &DatType) -> Result Result { let datum_binary: &Vec = object.expect_binary(); let hdr_size = header_size(); + let total_len = hdr_size + datum_binary.len(); + if buf.len() < total_len { + return Err(TyErr::new( + TyEC::InsufficientSpace, + "insufficient space".to_string(), + )); + } let offset = hdr_size as u32; - buf[offset as usize..].copy_from_slice(datum_binary); - let binary_bytes = BinSize::new(offset); + buf[offset as usize..offset as usize + datum_binary.len()].copy_from_slice(datum_binary); + let binary_bytes = BinSize::new(total_len as u32); binary_bytes.copy_to_slice(&mut buf[0..BinSize::size_of()]); - Ok(offset) + Ok(total_len as u32) } pub fn fn_binary_recv(buf: &[u8], _: &DatType) -> Result<(DatValue, u32), TyErr> { @@ -147,16 +154,17 @@ pub fn fn_binary_recv(buf: &[u8], _: &DatType) -> Result<(DatValue, u32), TyErr> } let binary_bytes = BinSize::from_slice(&buf[0..BinSize::size_of()]).size(); - if buf.len() < binary_bytes as usize { + if buf.len() < binary_bytes as usize || (binary_bytes as usize) < header_size() { return Err(TyErr::new( TyEC::InsufficientSpace, "space insufficient error".to_string(), )); } - let mut binary = Vec::with_capacity(binary_bytes as usize); - binary.resize(binary_bytes as usize, 0); - binary.copy_from_slice(&buf[header_size()..]); + let data_len = binary_bytes as usize - header_size(); + let mut binary = Vec::with_capacity(data_len); + binary.resize(data_len, 0); + binary.copy_from_slice(&buf[header_size()..binary_bytes as usize]); Ok((DatValue::from_binary(binary), binary_bytes)) } diff --git a/mudu_type/src/dt_impl/fn_f32.rs b/mudu_type/src/dt_impl/fn_f32.rs index 7b5070e..a11199c 100644 --- a/mudu_type/src/dt_impl/fn_f32.rs +++ b/mudu_type/src/dt_impl/fn_f32.rs @@ -33,7 +33,7 @@ pub fn fn_f32_in_json(v: &JsonValue, _: &DatType) -> Result { } }; match opt_f64 { - Some(num) => Ok(DatValue::from_f64(num)), + Some(num) => Ok(DatValue::from_f32(num as f32)), None => Err(TyErr::new( TyEC::TypeConvertFailed, format!("cannot convert json {} to f32", v.to_string()), @@ -84,7 +84,7 @@ pub fn fn_f32_send(v: &DatValue, _: &DatType) -> Result { pub fn fn_f32_send_to(v: &DatValue, _: &DatType, buf: &mut [u8]) -> Result { let i = v.to_f32(); let len = size_of_val(&i) as u32; - if size_of_val(&i) < buf.len() { + if buf.len() < size_of_val(&i) { return Err(TyErr::new( TyEC::InsufficientSpace, "insufficient space".to_string(), @@ -95,7 +95,7 @@ pub fn fn_f32_send_to(v: &DatValue, _: &DatType, buf: &mut [u8]) -> Result Result<(DatValue, u32), TyErr> { - if size_of::() < buf.len() { + if buf.len() < size_of::() { return Err(TyErr::new( TyEC::InsufficientSpace, "insufficient space".to_string(), diff --git a/mudu_type/src/dt_impl/fn_f32_arb.rs b/mudu_type/src/dt_impl/fn_f32_arb.rs index 4e51531..9864e4b 100644 --- a/mudu_type/src/dt_impl/fn_f32_arb.rs +++ b/mudu_type/src/dt_impl/fn_f32_arb.rs @@ -4,13 +4,18 @@ use crate::dat_value::DatValue; use crate::dt_fn_arbitrary::FnArbitrary; use arbitrary::{Arbitrary, Unstructured}; +fn arb_finite_f32(u: &mut Unstructured) -> arbitrary::Result { + let value = f32::arbitrary(u)?; + Ok(if value.is_finite() { value } else { 0.0 }) +} + pub fn fn_f32_arb_val(u: &mut Unstructured, dat_type: &DatType) -> arbitrary::Result { - Ok(DatValue::from_datum(f32::arbitrary(u)?, dat_type) + Ok(DatValue::from_datum(arb_finite_f32(u)?, dat_type) .map_err(|_| arbitrary::Error::IncorrectFormat)?) } pub fn fn_f32_arb_printable(u: &mut Unstructured, _: &DatType) -> arbitrary::Result { - Ok(f32::arbitrary(u)?.to_string()) + Ok(arb_finite_f32(u)?.to_string()) } pub fn fn_f32_arb_dt_param(_u: &mut Unstructured) -> arbitrary::Result { diff --git a/mudu_type/src/dt_impl/fn_f64.rs b/mudu_type/src/dt_impl/fn_f64.rs index 092b8fc..b138421 100644 --- a/mudu_type/src/dt_impl/fn_f64.rs +++ b/mudu_type/src/dt_impl/fn_f64.rs @@ -96,7 +96,7 @@ pub fn fn_f64_send_to(v: &DatValue, _: &DatType, buf: &mut [u8]) -> Result Result<(DatValue, u32), TyErr> { - if size_of::() < buf.len() { + if buf.len() < size_of::() { return Err(TyErr::new( TyEC::InsufficientSpace, "insufficient space".to_string(), diff --git a/mudu_type/src/dt_impl/fn_f64_arb.rs b/mudu_type/src/dt_impl/fn_f64_arb.rs index 5224424..215acaf 100644 --- a/mudu_type/src/dt_impl/fn_f64_arb.rs +++ b/mudu_type/src/dt_impl/fn_f64_arb.rs @@ -4,12 +4,17 @@ use crate::dat_value::DatValue; use crate::dt_fn_arbitrary::FnArbitrary; use arbitrary::{Arbitrary, Unstructured}; +fn arb_finite_f64(u: &mut Unstructured) -> arbitrary::Result { + let value = f64::arbitrary(u)?; + Ok(if value.is_finite() { value } else { 0.0 }) +} + pub fn fn_f64_arb_val(u: &mut Unstructured, _: &DatType) -> arbitrary::Result { - Ok(DatValue::from_f64(f64::arbitrary(u)?)) + Ok(DatValue::from_f64(arb_finite_f64(u)?)) } pub fn fn_f64_arb_printable(u: &mut Unstructured, _: &DatType) -> arbitrary::Result { - Ok(f64::arbitrary(u)?.to_string()) + Ok(arb_finite_f64(u)?.to_string()) } pub fn fn_f64_arb_dt_param(_u: &mut Unstructured) -> arbitrary::Result { diff --git a/mudu_type/src/dt_impl/fn_i128_arb.rs b/mudu_type/src/dt_impl/fn_i128_arb.rs index 7a633d8..d2e1992 100644 --- a/mudu_type/src/dt_impl/fn_i128_arb.rs +++ b/mudu_type/src/dt_impl/fn_i128_arb.rs @@ -9,7 +9,7 @@ pub fn fn_i128_arb_val(u: &mut Unstructured, _: &DatType) -> arbitrary::Result arbitrary::Result { - Ok(i128::arbitrary(u)?.to_string()) + Ok(format!("\"{}\"", i128::arbitrary(u)?)) } pub fn fn_i128_arb_dt_param(_u: &mut Unstructured) -> arbitrary::Result { diff --git a/mudu_type/src/dt_impl/fn_i32.rs b/mudu_type/src/dt_impl/fn_i32.rs index 357cd0c..1512059 100644 --- a/mudu_type/src/dt_impl/fn_i32.rs +++ b/mudu_type/src/dt_impl/fn_i32.rs @@ -100,7 +100,7 @@ pub fn fn_i32_send_to(v: &DatValue, _: &DatType, buf: &mut [u8]) -> Result Result<(DatValue, u32), TyErr> { - if size_of::() < buf.len() { + if buf.len() < size_of::() { return Err(TyErr::new( TyEC::InsufficientSpace, "insufficient space".to_string(), diff --git a/mudu_type/src/dt_impl/fn_i64.rs b/mudu_type/src/dt_impl/fn_i64.rs index d7e39da..d319da4 100644 --- a/mudu_type/src/dt_impl/fn_i64.rs +++ b/mudu_type/src/dt_impl/fn_i64.rs @@ -88,7 +88,7 @@ fn fn_i64_send(v: &DatValue, _: &DatType) -> Result { fn fn_i64_send_to(v: &DatValue, _: &DatType, buf: &mut [u8]) -> Result { let i = v.to_i64(); let len = size_of_val(&i) as u32; - if size_of_val(&i) < buf.len() { + if buf.len() < size_of_val(&i) { return Err(TyErr::new( TyEC::InsufficientSpace, "insufficient space".to_string(), @@ -99,7 +99,7 @@ fn fn_i64_send_to(v: &DatValue, _: &DatType, buf: &mut [u8]) -> Result Result<(DatValue, u32), TyErr> { - if size_of::() < buf.len() { + if buf.len() < size_of::() { return Err(TyErr::new( TyEC::InsufficientSpace, "insufficient space".to_string(), diff --git a/mudu_type/src/dt_impl/fn_object_arb.rs b/mudu_type/src/dt_impl/fn_object_arb.rs index 11b2c85..2d686c8 100644 --- a/mudu_type/src/dt_impl/fn_object_arb.rs +++ b/mudu_type/src/dt_impl/fn_object_arb.rs @@ -1,18 +1,80 @@ use crate::dat_type::DatType; +use crate::dat_type_id::DatTypeID; use crate::dat_value::DatValue; use crate::dt_fn_arbitrary::FnArbitrary; +use crate::dt_impl::dt_create::create_object_type; +use crate::dt_impl::fn_object::fn_object_out; +use crate::type_error::TyErr; +use arbitrary::Arbitrary; use arbitrary::Unstructured; -pub fn fn_object_arb_typed(_: &mut Unstructured, _: &DatType) -> arbitrary::Result { - todo!() +const OBJECT_FIELD_TYPE_IDS: [DatTypeID; 9] = [ + DatTypeID::I32, + DatTypeID::I64, + DatTypeID::F32, + DatTypeID::F64, + DatTypeID::String, + DatTypeID::U128, + DatTypeID::I128, + DatTypeID::Binary, + DatTypeID::Array, +]; + +fn arbitrary_name(u: &mut Unstructured, prefix: &str, index: usize) -> arbitrary::Result { + let len = (u8::arbitrary(u)? as usize % 8) + 1; + let mut s = String::with_capacity(prefix.len() + len + 8); + s.push_str(prefix); + s.push('_'); + s.push_str(&index.to_string()); + s.push('_'); + for _ in 0..len { + let ch = (u8::arbitrary(u)? % 26) + b'a'; + s.push(ch as char); + } + Ok(s) +} + +fn arbitrary_field_type(u: &mut Unstructured) -> arbitrary::Result { + let index = (u8::arbitrary(u)? as usize) % OBJECT_FIELD_TYPE_IDS.len(); + let type_id = OBJECT_FIELD_TYPE_IDS[index]; + if type_id.has_param() { + type_id.fn_arb_param()(u) + } else { + Ok(DatType::default_for(type_id)) + } +} + +fn to_arb_err(e: TyErr) -> arbitrary::Error { + let _ = e; + arbitrary::Error::IncorrectFormat +} + +pub fn fn_object_arb_typed(u: &mut Unstructured, dat_type: &DatType) -> arbitrary::Result { + let param = dat_type.expect_record_param(); + let mut fields = Vec::with_capacity(param.fields().len()); + for (_, field_ty) in param.fields() { + let value = field_ty.dat_type_id().fn_arb_internal()(u, field_ty)?; + fields.push(value); + } + Ok(DatValue::from_record(fields)) } -pub fn fn_object_arb_printable(_: &mut Unstructured, _: &DatType) -> arbitrary::Result { - todo!() +pub fn fn_object_arb_printable(u: &mut Unstructured, dat_type: &DatType) -> arbitrary::Result { + let value = fn_object_arb_typed(u, dat_type)?; + let textual = fn_object_out(&value, dat_type).map_err(to_arb_err)?; + Ok(textual.into()) } -pub fn fn_object_arb_dt_param(_: &mut Unstructured) -> arbitrary::Result { - todo!() +pub fn fn_object_arb_dt_param(u: &mut Unstructured) -> arbitrary::Result { + let field_count = (u8::arbitrary(u)? as usize % 4) + 1; + let name = arbitrary_name(u, "record", 0)?; + let mut fields = Vec::with_capacity(field_count); + for idx in 0..field_count { + let field_name = arbitrary_name(u, "field", idx)?; + let field_ty = arbitrary_field_type(u)?; + fields.push((field_name, field_ty)); + } + Ok(create_object_type(name, fields)) } pub const FN_OBJECT_ARBITRARY: FnArbitrary = FnArbitrary { diff --git a/mudu_type/src/dt_impl/fn_string_arb.rs b/mudu_type/src/dt_impl/fn_string_arb.rs index 8443025..7cd3bb4 100644 --- a/mudu_type/src/dt_impl/fn_string_arb.rs +++ b/mudu_type/src/dt_impl/fn_string_arb.rs @@ -28,7 +28,7 @@ pub fn fn_char_arb_val(u: &mut Unstructured, param: &DatType) -> arbitrary::Resu pub fn fn_char_arb_printable(u: &mut Unstructured, param: &DatType) -> arbitrary::Result { let length = param_len(param).unwrap(); let s = _arbitrary_string(u, length as usize)?; - Ok(format!("\"{}\"", s)) + serde_json::to_string(&s).map_err(|_| arbitrary::Error::IncorrectFormat) } pub fn fn_char_arb_dt_param(u: &mut Unstructured) -> arbitrary::Result { diff --git a/mudu_type/src/dt_impl/fn_u128_arb.rs b/mudu_type/src/dt_impl/fn_u128_arb.rs index 320b8bf..3a07d61 100644 --- a/mudu_type/src/dt_impl/fn_u128_arb.rs +++ b/mudu_type/src/dt_impl/fn_u128_arb.rs @@ -9,7 +9,7 @@ pub fn fn_u128_arb_val(u: &mut Unstructured, _: &DatType) -> arbitrary::Result arbitrary::Result { - Ok(u128::arbitrary(u)?.to_string()) + Ok(format!("\"{}\"", u128::arbitrary(u)?)) } pub fn fn_u128_arb_dt_param(_u: &mut Unstructured) -> arbitrary::Result { diff --git a/mudu_type/src/dt_impl/generic_prop_test.rs b/mudu_type/src/dt_impl/generic_prop_test.rs new file mode 100644 index 0000000..16655a2 --- /dev/null +++ b/mudu_type/src/dt_impl/generic_prop_test.rs @@ -0,0 +1,244 @@ +use crate::dat_type::DatType; +use crate::dat_type_id::DatTypeID; +use crate::datum::DatumDyn; +use arbitrary::Unstructured; + +const SEED_COUNT: u64 = 32; +const SEED_BYTES_LEN: usize = 512; + +fn supported_scalar_type_ids() -> &'static [DatTypeID] { + &[ + DatTypeID::I32, + DatTypeID::I64, + DatTypeID::F32, + DatTypeID::F64, + DatTypeID::String, + DatTypeID::U128, + DatTypeID::I128, + DatTypeID::Binary, + ] +} + +fn supported_complex_type_ids() -> &'static [DatTypeID] { + &[DatTypeID::Array, DatTypeID::Record] +} + +fn seed_bytes(seed: u64, len: usize) -> Vec { + let mut state = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15); + let mut out = Vec::with_capacity(len); + for _ in 0..len { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + out.push((state & 0xff) as u8); + } + out +} + +fn canonical_binary(id: DatTypeID, dt: &DatType, value: &crate::dat_value::DatValue) -> Vec { + id.fn_send()(value, dt).unwrap().as_ref().to_vec() +} + +fn assert_binary_roundtrip(id: DatTypeID, dt: &DatType, value: &crate::dat_value::DatValue) { + let binary = id.fn_send()(value, dt).unwrap(); + let (decoded, used) = id.fn_recv()(binary.as_ref(), dt).unwrap(); + assert_eq!(used as usize, binary.as_ref().len(), "binary recv size mismatch for {:?}", id); + assert_eq!( + canonical_binary(id, dt, &decoded), + binary.as_ref(), + "binary roundtrip mismatch for {:?}", + id + ); + + let mut buf = vec![0u8; binary.as_ref().len()]; + let sent = id.fn_send_to()(value, dt, &mut buf).unwrap(); + assert_eq!(sent as usize, binary.as_ref().len(), "send_to len mismatch for {:?}", id); + assert_eq!(buf, binary.as_ref(), "send_to bytes mismatch for {:?}", id); +} + +fn assert_textual_roundtrip(id: DatTypeID, dt: &DatType, value: &crate::dat_value::DatValue) { + let textual = id.fn_output()(value, dt).unwrap().into(); + let decoded = id.fn_input()(&textual, dt).unwrap(); + assert_eq!( + decoded.dat_type_id().unwrap(), + id, + "textual parse type mismatch for {:?}", + id + ); + let textual2 = id.fn_output()(&decoded, dt).unwrap().into(); + let decoded2 = id.fn_input()(&textual2, dt).unwrap(); + assert_eq!( + decoded2.dat_type_id().unwrap(), + id, + "textual reparse type mismatch for {:?}", + id + ); +} + +fn assert_json_roundtrip(id: DatTypeID, dt: &DatType, value: &crate::dat_value::DatValue) { + let json = id.fn_output_json()(value, dt).unwrap().into_json_value(); + let decoded = id.fn_input_json()(&json, dt).unwrap(); + assert_eq!( + decoded.dat_type_id().unwrap(), + id, + "json parse type mismatch for {:?}", + id + ); + let json2 = id.fn_output_json()(&decoded, dt).unwrap().into_json_value(); + let decoded2 = id.fn_input_json()(&json2, dt).unwrap(); + assert_eq!( + decoded2.dat_type_id().unwrap(), + id, + "json reparse type mismatch for {:?}", + id + ); +} + +fn assert_msgpack_roundtrip(id: DatTypeID, dt: &DatType, value: &crate::dat_value::DatValue) { + let msgpack = id.fn_output_msg_pack()(value, dt).unwrap(); + let decoded = id.fn_input_msg_pack()(&msgpack, dt).unwrap(); + assert_eq!( + decoded.dat_type_id().unwrap(), + id, + "msgpack parse type mismatch for {:?}", + id + ); + let msgpack2 = id.fn_output_msg_pack()(&decoded, dt).unwrap(); + let decoded2 = id.fn_input_msg_pack()(&msgpack2, dt).unwrap(); + assert_eq!( + decoded2.dat_type_id().unwrap(), + id, + "msgpack reparse type mismatch for {:?}", + id + ); +} + +fn assert_default_is_sendable(id: DatTypeID, dt: &DatType) { + let value = id.fn_default()(dt).unwrap(); + assert_eq!(value.dat_type_id().unwrap(), id, "default type mismatch for {:?}", id); + + let binary = id.fn_send()(&value, dt).unwrap(); + let len = id.fn_send_dat_len()(&value, dt).unwrap(); + assert_eq!(binary.as_ref().len(), len as usize, "default data len mismatch for {:?}", id); + + if let Some(type_len) = id.fn_send_type_len()(dt).unwrap() { + assert_eq!( + binary.as_ref().len(), + type_len as usize, + "default fixed len mismatch for {:?}", + id + ); + } +} + +#[test] +fn supported_scalar_arbitrary_values_cover_roundtrip_paths() { + for &id in supported_scalar_type_ids() { + for seed in 0..SEED_COUNT { + let bytes = seed_bytes(seed ^ id.to_u32() as u64, SEED_BYTES_LEN); + let mut u = Unstructured::new(&bytes); + let dt = id.fn_arb_param()(&mut u).unwrap(); + assert_eq!(dt.dat_type_id(), id, "arb param type mismatch for {:?}", id); + + let value = match id.fn_arb_internal()(&mut u, &dt) { + Ok(value) => value, + Err(arbitrary::Error::NotEnoughData) => continue, + Err(err) => panic!("arb value failed for {:?}: {:?}", id, err), + }; + assert_eq!(value.dat_type_id().unwrap(), id, "arb value type mismatch for {:?}", id); + + assert_binary_roundtrip(id, &dt, &value); + assert_textual_roundtrip(id, &dt, &value); + assert_json_roundtrip(id, &dt, &value); + assert_msgpack_roundtrip(id, &dt, &value); + } + } +} + +#[test] +fn supported_scalar_printable_values_parse_back() { + for &id in supported_scalar_type_ids() { + for seed in 0..SEED_COUNT { + let bytes = seed_bytes((seed << 8) ^ id.to_u32() as u64, SEED_BYTES_LEN); + let mut u = Unstructured::new(&bytes); + let dt = id.fn_arb_param()(&mut u).unwrap(); + let printable = match id.fn_arb_printable()(&mut u, &dt) { + Ok(printable) => printable, + Err(arbitrary::Error::NotEnoughData) => continue, + Err(err) => panic!("arb printable failed for {:?}: {:?}", id, err), + }; + let value = id.fn_input()(&printable, &dt).unwrap(); + assert_eq!( + value.dat_type_id().unwrap(), + id, + "printable parse type mismatch for {:?}", + id + ); + assert_textual_roundtrip(id, &dt, &value); + } + } +} + +#[test] +fn supported_scalar_default_values_are_sendable() { + for &id in supported_scalar_type_ids() { + for seed in 0..SEED_COUNT { + let bytes = seed_bytes((seed << 16) ^ id.to_u32() as u64, SEED_BYTES_LEN); + let mut u = Unstructured::new(&bytes); + let dt = id.fn_arb_param()(&mut u).unwrap(); + assert_default_is_sendable(id, &dt); + } + } +} + +#[test] +fn supported_complex_values_cover_roundtrip_paths() { + for &id in supported_complex_type_ids() { + for seed in 0..SEED_COUNT { + let bytes = seed_bytes(seed ^ (id.to_u32() as u64) << 24, SEED_BYTES_LEN); + let mut u = Unstructured::new(&bytes); + let dt = id.fn_arb_param()(&mut u).unwrap(); + let value = match id.fn_arb_internal()(&mut u, &dt) { + Ok(value) => value, + Err(arbitrary::Error::NotEnoughData) => continue, + Err(err) => panic!("complex arb value failed for {:?}: {:?}", id, err), + }; + assert_eq!( + value.dat_type_id().unwrap(), + id, + "complex arb value type mismatch for {:?}", + id + ); + + assert_binary_roundtrip(id, &dt, &value); + assert_textual_roundtrip(id, &dt, &value); + assert_json_roundtrip(id, &dt, &value); + assert_msgpack_roundtrip(id, &dt, &value); + assert_default_is_sendable(id, &dt); + } + } +} + +#[test] +fn supported_complex_printable_values_parse_back() { + for &id in supported_complex_type_ids() { + for seed in 0..SEED_COUNT { + let bytes = seed_bytes((seed << 32) ^ id.to_u32() as u64, SEED_BYTES_LEN); + let mut u = Unstructured::new(&bytes); + let dt = id.fn_arb_param()(&mut u).unwrap(); + let printable = match id.fn_arb_printable()(&mut u, &dt) { + Ok(printable) => printable, + Err(arbitrary::Error::NotEnoughData) => continue, + Err(err) => panic!("complex arb printable failed for {:?}: {:?}", id, err), + }; + let value = id.fn_input()(&printable, &dt).unwrap(); + assert_eq!( + value.dat_type_id().unwrap(), + id, + "complex printable parse type mismatch for {:?}", + id + ); + assert_textual_roundtrip(id, &dt, &value); + } + } +} diff --git a/mudu_type/src/dt_impl/mod.rs b/mudu_type/src/dt_impl/mod.rs index fac7cff..1fdff3d 100644 --- a/mudu_type/src/dt_impl/mod.rs +++ b/mudu_type/src/dt_impl/mod.rs @@ -36,3 +36,18 @@ mod fn_object_param; mod fn_string_arb; #[cfg(any(test, feature = "test"))] mod fn_u128_arb; + +#[cfg(test)] +mod generic_prop_test; + +#[cfg(test)] +mod object_array_test; + +#[cfg(test)] +mod param_test; + +#[cfg(test)] +mod compare_test; + +#[cfg(test)] +mod error_test; diff --git a/mudu_type/src/dt_impl/object_array_test.rs b/mudu_type/src/dt_impl/object_array_test.rs new file mode 100644 index 0000000..bf9a643 --- /dev/null +++ b/mudu_type/src/dt_impl/object_array_test.rs @@ -0,0 +1,141 @@ +use crate::dat_type::DatType; +use crate::dat_type_id::DatTypeID; +use crate::dat_value::DatValue; +use crate::dt_impl::dt_create::{create_array_type, create_object_type, create_string_type}; +use arbitrary::Unstructured; + +fn seeded_unstructured(seed: u64) -> Unstructured<'static> { + let mut state = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15); + let mut bytes = Vec::with_capacity(256); + for _ in 0..256 { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + bytes.push((state & 0xff) as u8); + } + Unstructured::new(Box::leak(bytes.into_boxed_slice())) +} + +fn assert_binary_roundtrip(id: DatTypeID, dt: &DatType, value: &DatValue) { + let binary = id.fn_send()(value, dt).unwrap(); + let (decoded, used) = id.fn_recv()(binary.as_ref(), dt).unwrap(); + assert_eq!(used as usize, binary.as_ref().len()); + let binary2 = id.fn_send()(&decoded, dt).unwrap(); + assert_eq!(binary.as_ref(), binary2.as_ref()); +} + +#[test] +fn array_arb_param_produces_supported_inner_type() { + for seed in 0..32 { + let mut u = seeded_unstructured(seed); + let dt = DatTypeID::Array.fn_arb_param()(&mut u).unwrap(); + assert_eq!(dt.dat_type_id(), DatTypeID::Array); + let inner = dt.expect_array_param().dat_type(); + assert!(matches!( + inner.dat_type_id(), + DatTypeID::I32 + | DatTypeID::I64 + | DatTypeID::F32 + | DatTypeID::F64 + | DatTypeID::String + | DatTypeID::U128 + | DatTypeID::I128 + | DatTypeID::Binary + )); + } +} + +#[test] +fn array_roundtrip_with_variable_width_inner_type() { + let dt = create_array_type(create_string_type(Some(12))); + let value = DatValue::from_array(vec![ + DatValue::from_string("alpha".to_string()), + DatValue::from_string(String::new()), + DatValue::from_string("zeta".to_string()), + ]); + + assert_binary_roundtrip(DatTypeID::Array, &dt, &value); + + let textual = DatTypeID::Array.fn_output()(&value, &dt).unwrap(); + let parsed = DatTypeID::Array.fn_input()(textual.as_ref(), &dt).unwrap(); + assert_eq!( + DatTypeID::Array.fn_send()(&parsed, &dt).unwrap().as_ref(), + DatTypeID::Array.fn_send()(&value, &dt).unwrap().as_ref() + ); +} + +#[test] +fn object_arb_param_produces_named_fields() { + for seed in 100..132 { + let mut u = seeded_unstructured(seed); + let dt = DatTypeID::Record.fn_arb_param()(&mut u).unwrap(); + let record = dt.expect_record_param(); + assert_eq!(dt.dat_type_id(), DatTypeID::Record); + assert!(!record.record_name().is_empty()); + assert!(!record.fields().is_empty()); + for (name, field_ty) in record.fields() { + assert!(!name.is_empty()); + assert!(matches!( + field_ty.dat_type_id(), + DatTypeID::I32 + | DatTypeID::I64 + | DatTypeID::F32 + | DatTypeID::F64 + | DatTypeID::String + | DatTypeID::U128 + | DatTypeID::I128 + | DatTypeID::Binary + | DatTypeID::Array + )); + } + } +} + +#[test] +fn object_roundtrip_with_nested_array_field() { + let score_type = create_array_type(DatType::default_for(DatTypeID::I32)); + let dt = create_object_type( + "player".to_string(), + vec![ + ("name".to_string(), create_string_type(Some(16))), + ("scores".to_string(), score_type.clone()), + ("blob".to_string(), DatType::new_no_param(DatTypeID::Binary)), + ], + ); + let value = DatValue::from_record(vec![ + DatValue::from_string("neo".to_string()), + DatValue::from_array(vec![ + DatValue::from_i32(7), + DatValue::from_i32(11), + DatValue::from_i32(-3), + ]), + DatValue::from_binary(vec![1, 2, 3, 5, 8]), + ]); + + assert_binary_roundtrip(DatTypeID::Record, &dt, &value); + + let json = DatTypeID::Record.fn_output_json()(&value, &dt).unwrap(); + let parsed = DatTypeID::Record + .fn_input_json()(&json.into_json_value(), &dt) + .unwrap(); + assert_eq!( + DatTypeID::Record.fn_send()(&parsed, &dt).unwrap().as_ref(), + DatTypeID::Record.fn_send()(&value, &dt).unwrap().as_ref() + ); +} + +#[test] +fn object_arbitrary_value_matches_generated_schema() { + for seed in 200..216 { + let mut u = seeded_unstructured(seed); + let dt = DatTypeID::Record.fn_arb_param()(&mut u).unwrap(); + let value = match DatTypeID::Record.fn_arb_internal()(&mut u, &dt) { + Ok(value) => value, + Err(arbitrary::Error::NotEnoughData) => continue, + Err(err) => panic!("unexpected arbitrary error: {:?}", err), + }; + let record = value.expect_record(); + assert_eq!(record.len(), dt.expect_record_param().fields().len()); + assert_binary_roundtrip(DatTypeID::Record, &dt, &value); + } +} diff --git a/mudu_type/src/dt_impl/param_test.rs b/mudu_type/src/dt_impl/param_test.rs new file mode 100644 index 0000000..2a8948a --- /dev/null +++ b/mudu_type/src/dt_impl/param_test.rs @@ -0,0 +1,66 @@ +use crate::dat_type::DatType; +use crate::dat_type_id::DatTypeID; +use crate::dt_impl::dt_create::{create_array_type, create_object_type, create_string_type}; +use mudu::common::default_value::DT_CHAR_FIXED_LEN_DEFAULT; + +fn assert_param_input_roundtrip(id: DatTypeID, dt: DatType) { + let info = dt.to_info(); + let input = id.opt_fn_param().as_ref().unwrap().input; + let parsed = input(&info.param).unwrap(); + + assert_eq!(parsed.dat_type_id(), id); + assert_eq!(parsed.to_info().id, info.id); + assert_eq!(parsed.to_info().param, info.param); + + let reparsed = DatType::from_info(&parsed.to_info()).unwrap(); + assert_eq!(reparsed.to_info().id, info.id); + assert_eq!(reparsed.to_info().param, info.param); +} + +#[test] +fn string_param_input_parses_and_roundtrips() { + assert_param_input_roundtrip(DatTypeID::String, create_string_type(Some(48))); +} + +#[test] +fn string_param_default_matches_registered_default() { + let default = DatTypeID::String.fn_param_default().unwrap()(); + assert_eq!(default.dat_type_id(), DatTypeID::String); + + let string_param = default.expect_string_param(); + assert_eq!(string_param.length(), DT_CHAR_FIXED_LEN_DEFAULT as u32); +} + +#[test] +fn array_param_input_parses_nested_type() { + let dt = create_array_type(create_string_type(Some(16))); + assert_param_input_roundtrip(DatTypeID::Array, dt); +} + +#[test] +fn object_param_input_parses_record_schema() { + let dt = create_object_type( + "user_profile".to_string(), + vec![ + ("name".to_string(), create_string_type(Some(32))), + ( + "tags".to_string(), + create_array_type(DatType::new_no_param(DatTypeID::Binary)), + ), + ("age".to_string(), DatType::new_no_param(DatTypeID::I32)), + ], + ); + assert_param_input_roundtrip(DatTypeID::Record, dt); +} + +#[test] +fn param_input_rejects_invalid_json() { + let string_err = (DatTypeID::String.opt_fn_param().as_ref().unwrap().input)("{"); + assert!(string_err.is_err()); + + let array_err = (DatTypeID::Array.opt_fn_param().as_ref().unwrap().input)("{"); + assert!(array_err.is_err()); + + let record_err = (DatTypeID::Record.opt_fn_param().as_ref().unwrap().input)("{"); + assert!(record_err.is_err()); +} diff --git a/mudu_utils/src/sync/async_task.rs b/mudu_utils/src/sync/async_task.rs index 1bf0a9a..38040c6 100644 --- a/mudu_utils/src/sync/async_task.rs +++ b/mudu_utils/src/sync/async_task.rs @@ -1,12 +1,11 @@ use crate::notifier::{NotifyWait, Waiter}; use crate::sync::unique_inner::UniqueInner; use crate::task::{spawn_local_task, spawn_task}; -use futures::future::select_all; +use futures::future::try_join_all; use mudu::common::result::RS; use mudu::error::ec::EC; use mudu::m_error; use std::any::Any; -use std::pin::Pin; use tokio::task::{JoinHandle, LocalSet}; pub trait Task: Any {} @@ -134,44 +133,30 @@ impl TaskWrapper { } pub async fn join_all(result: Vec) -> RS<()> { - let mut result = result; - let mut local_sets = vec![]; - for r in result.iter_mut() { - let mut opt = None; - std::mem::swap(&mut r.opt_local, &mut opt); - match opt { - Some(r) => { - local_sets.push(r); + let futures = result.into_iter().map(|r| async move { + let AsyncResult { + opt_local, + join_handle, + } = r; + match opt_local { + Some(local_set) => { + let _opt = local_set + .run_until(async move { + join_handle + .await + .map_err(|e| m_error!(EC::InternalErr, "join error", e)) + }) + .await?; + } + None => { + let _opt = join_handle + .await + .map_err(|e| m_error!(EC::InternalErr, "join error", e))?; } - None => {} } - } - let mut pinned_futures: Vec>>> = Vec::new(); - - for ls in local_sets { - let future = async move { - ls.run_until(std::future::pending::<()>()).await; - }; - pinned_futures.push(Box::pin(future)); - } - // wait local set - while !pinned_futures.is_empty() { - let (_, index, remaining) = select_all(pinned_futures).await; - println!( - "One LocalSet {} mpleted, {} remaining", - index, - remaining.len() - ); - pinned_futures = remaining; - } - // wait all task join - for r in result.into_iter() { - let _opt = r - .join_handle - .await - .map_err(|e| m_error!(EC::InternalErr, "join error", e))?; - } - + Ok(()) + }); + try_join_all(futures).await?; Ok(()) } diff --git a/mudu_utils/src/sync/notify_wait.rs b/mudu_utils/src/sync/notify_wait.rs index d4ad2f6..1c689ec 100644 --- a/mudu_utils/src/sync/notify_wait.rs +++ b/mudu_utils/src/sync/notify_wait.rs @@ -103,3 +103,23 @@ impl _LockNotify { } } } + +#[cfg(test)] +mod tests { + use super::create_notify_wait; + + #[tokio::test] + async fn notify_wait_delivers_value_once() { + let (notify, wait) = create_notify_wait::(); + assert!(notify.notify(7).unwrap()); + assert_eq!(wait.wait().await.unwrap(), Some(7)); + assert_eq!(wait.wait().await.unwrap(), None); + } + + #[tokio::test] + async fn notify_returns_false_after_receiver_is_dropped() { + let (notify, wait) = create_notify_wait::(); + drop(wait); + assert!(!notify.notify(9).unwrap()); + } +} diff --git a/mudu_utils/src/sync/s_task.rs b/mudu_utils/src/sync/s_task.rs index 62bf3df..b393cc2 100644 --- a/mudu_utils/src/sync/s_task.rs +++ b/mudu_utils/src/sync/s_task.rs @@ -28,3 +28,43 @@ impl STaskRef for UniqueInner { } pub type SyncTask = Arc; + +#[cfg(test)] +mod tests { + use super::STaskRef; + use crate::sync::unique_inner::UniqueInner; + use mudu::common::result::RS; + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + + struct DemoTask { + name: String, + runs: Arc, + } + + impl super::STask for DemoTask { + fn name(&self) -> String { + self.name.clone() + } + + fn run(self) -> RS<()> { + self.runs.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + } + + #[test] + fn unique_inner_task_ref_exposes_name_and_runs_once() { + let runs = Arc::new(AtomicUsize::new(0)); + let task = UniqueInner::new(DemoTask { + name: "demo".to_string(), + runs: runs.clone(), + }); + + assert_eq!(task.name(), "demo"); + task.run_once().unwrap(); + assert_eq!(runs.load(Ordering::SeqCst), 1); + } +} diff --git a/sql_parser/src/ast/ast_node.rs b/sql_parser/src/ast/ast_node.rs index 86176e3..59c783b 100644 --- a/sql_parser/src/ast/ast_node.rs +++ b/sql_parser/src/ast/ast_node.rs @@ -3,8 +3,3 @@ use std::fmt::Debug; pub trait ASTNode: Any + Debug + Send + Sync {} -pub fn ast_cast_to(_expr: Box) -> Result, Box> { - //let any: Box = expr; - //any.downcast::() - todo!() -} diff --git a/sql_parser/src/ast/expr_arithmetic.rs b/sql_parser/src/ast/expr_arithmetic.rs index 2ec5e91..a2ae32b 100644 --- a/sql_parser/src/ast/expr_arithmetic.rs +++ b/sql_parser/src/ast/expr_arithmetic.rs @@ -41,3 +41,32 @@ impl Debug for ExprArithmetic { } impl ASTNode for ExprArithmetic {} + +#[cfg(test)] +mod tests { + use super::ExprArithmetic; + use crate::ast::expr_item::{ExprItem, ExprValue}; + use crate::ast::expr_literal::ExprLiteral; + use crate::ast::expr_operator::Arithmetic; + use crate::ast::expression::ExprType; + use std::sync::Arc; + use mudu_type::dat_typed::DatTyped; + + #[test] + fn arithmetic_expression_preserves_operands_and_operator() { + let left = ExprType::Value(Arc::new(ExprItem::ItemValue(ExprValue::ValueLiteral( + ExprLiteral::DatumLiteral(DatTyped::from_i32(1)), + )))); + let right = ExprType::Value(Arc::new(ExprItem::ItemValue(ExprValue::ValueLiteral( + ExprLiteral::DatumLiteral(DatTyped::from_i32(2)), + )))); + let expr = ExprArithmetic::new(Arithmetic::PLUS, left.clone(), right.clone()); + + assert!(matches!(expr.op(), Arithmetic::PLUS)); + assert!(matches!(expr.left(), ExprType::Value(_))); + assert!(matches!(expr.right(), ExprType::Value(_))); + + let debug = format!("{expr:?}"); + assert!(debug.contains("arithmetic op")); + } +} diff --git a/sql_parser/src/ast/expr_compare.rs b/sql_parser/src/ast/expr_compare.rs index a5a44b7..4d0ebb6 100644 --- a/sql_parser/src/ast/expr_compare.rs +++ b/sql_parser/src/ast/expr_compare.rs @@ -64,3 +64,41 @@ impl Debug for ExprCompare { } impl ASTNode for ExprCompare {} + +#[cfg(test)] +mod tests { + use super::ExprCompare; + use crate::ast::expr_item::{ExprItem, ExprValue}; + use crate::ast::expr_literal::ExprLiteral; + use crate::ast::expr_name::ExprName; + use crate::ast::expr_operator::ValueCompare; + use mudu_type::dat_typed::DatTyped; + + fn field(name: &str) -> ExprItem { + let mut expr = ExprName::new(); + expr.set_name(name.to_string()); + ExprItem::ItemName(expr) + } + + fn literal_i32(value: i32) -> ExprItem { + ExprItem::ItemValue(ExprValue::ValueLiteral(ExprLiteral::DatumLiteral( + DatTyped::from_i32(value), + ))) + } + + #[test] + fn expr_field_op_literal_reverts_literal_field_order() { + let cmp = ExprCompare::new(ValueCompare::GT, literal_i32(7), field("id")); + let (field, literal, op) = cmp.expr_field_op_literal().unwrap(); + + assert_eq!(field.name(), "id"); + assert_eq!(literal.dat_type().dat_internal().to_i32(), 7); + assert!(matches!(op, ValueCompare::LE)); + } + + #[test] + fn expr_field_op_literal_rejects_non_literal_pairs() { + let cmp = ExprCompare::new(ValueCompare::EQ, field("lhs"), field("rhs")); + assert!(cmp.expr_field_op_literal().is_none()); + } +} diff --git a/sql_parser/src/ast/expr_item.rs b/sql_parser/src/ast/expr_item.rs index d5717fd..8271dbe 100644 --- a/sql_parser/src/ast/expr_item.rs +++ b/sql_parser/src/ast/expr_item.rs @@ -31,3 +31,39 @@ impl ExprItem { } } } + +#[cfg(test)] +mod tests { + use super::{ExprItem, ExprValue}; + use crate::ast::expr_literal::ExprLiteral; + use crate::ast::expr_name::ExprName; + use mudu_type::dat_type_id::DatTypeID; + use mudu_type::dat_typed::DatTyped; + + #[test] + fn expr_item_to_field_returns_name_only_for_name_variant() { + let mut field = ExprName::new(); + field.set_name("id".to_string()); + let name = ExprItem::ItemName(field); + assert_eq!(name.to_field().unwrap().name(), "id"); + + let literal = ExprItem::ItemValue(ExprValue::ValueLiteral(ExprLiteral::DatumLiteral( + DatTyped::from_i32(7), + ))); + assert!(literal.to_field().is_none()); + } + + #[test] + fn expr_item_to_literal_returns_literal_only_for_literal_variant() { + let literal = ExprItem::ItemValue(ExprValue::ValueLiteral(ExprLiteral::DatumLiteral( + DatTyped::from_string("alice".to_string()), + ))); + assert_eq!( + literal.to_literal().unwrap().dat_type().dat_type().dat_type_id(), + DatTypeID::String + ); + + let placeholder = ExprItem::ItemValue(ExprValue::ValuePlaceholder); + assert!(placeholder.to_literal().is_none()); + } +} diff --git a/sql_parser/src/ast/expr_literal.rs b/sql_parser/src/ast/expr_literal.rs index a97cc23..51f8245 100644 --- a/sql_parser/src/ast/expr_literal.rs +++ b/sql_parser/src/ast/expr_literal.rs @@ -15,3 +15,16 @@ impl ExprLiteral { } impl ASTNode for ExprLiteral {} + +#[cfg(test)] +mod tests { + use super::ExprLiteral; + use mudu_type::dat_type_id::DatTypeID; + use mudu_type::dat_typed::DatTyped; + + #[test] + fn expr_literal_returns_underlying_typed_value() { + let literal = ExprLiteral::DatumLiteral(DatTyped::from_i32(11)); + assert_eq!(literal.dat_type().dat_type().dat_type_id(), DatTypeID::I32); + } +} diff --git a/sql_parser/src/ast/expr_operator.rs b/sql_parser/src/ast/expr_operator.rs index 491d212..16bfbd9 100644 --- a/sql_parser/src/ast/expr_operator.rs +++ b/sql_parser/src/ast/expr_operator.rs @@ -79,7 +79,6 @@ impl Operator { } } - impl ValueCompare { pub fn revert_cmp_op(op: ValueCompare) -> ValueCompare { match op { @@ -91,4 +90,4 @@ impl ValueCompare { ValueCompare::NE => ValueCompare::NE, } } -} \ No newline at end of file +} diff --git a/sql_parser/src/ast/mod.rs b/sql_parser/src/ast/mod.rs index be82a58..ef91b52 100644 --- a/sql_parser/src/ast/mod.rs +++ b/sql_parser/src/ast/mod.rs @@ -22,11 +22,14 @@ mod expr_arithmetic; mod parser_test; pub mod stmt_copy_from; pub mod stmt_copy_to; +pub mod stmt_create_partition_placement; +pub mod stmt_create_partition_rule; pub mod stmt_drop; pub mod stmt_drop_table; pub mod stmt_insert; pub mod stmt_list; pub mod stmt_select; +pub mod stmt_table_partition; pub mod stmt_type; pub mod stmt_update; pub mod type_declare; diff --git a/sql_parser/src/ast/parser.rs b/sql_parser/src/ast/parser.rs index 9bd8786..15a00a3 100644 --- a/sql_parser/src/ast/parser.rs +++ b/sql_parser/src/ast/parser.rs @@ -15,12 +15,19 @@ use crate::ast::expr_visitor::ExprVisitor; use crate::ast::expression::ExprType; use crate::ast::select_term::SelectTerm; use crate::ast::stmt_copy_from::StmtCopyFrom; +use crate::ast::stmt_create_partition_placement::{ + StmtCreatePartitionPlacement, StmtPartitionPlacementItem, +}; +use crate::ast::stmt_create_partition_rule::{ + StmtCreatePartitionRule, StmtPartitionBound, StmtRangePartition, +}; use crate::ast::stmt_create_table::StmtCreateTable; use crate::ast::stmt_delete::StmtDelete; use crate::ast::stmt_drop_table::StmtDropTable; use crate::ast::stmt_insert::StmtInsert; use crate::ast::stmt_list::StmtList; use crate::ast::stmt_select::StmtSelect; +use crate::ast::stmt_table_partition::StmtTablePartition; use crate::ast::stmt_type::{StmtCommand, StmtType}; use crate::ast::stmt_update::{AssignedValue, Assignment, StmtUpdate}; use crate::ts_const::{ts_field_name, ts_kind_id}; @@ -176,6 +183,13 @@ impl SQLParser { } pub fn parse(&self, sql: &str) -> RS { + if let Some(stmt_list) = self.try_parse_custom_statement(sql)? { + return Ok(stmt_list); + } + self.parse_standard(sql) + } + + fn parse_standard(&self, sql: &str) -> RS { let parse_context = ParseContext::new(sql.to_string()); let mut guard = self.parser.lock().unwrap(); let opt_tree = guard.parse(sql, None); @@ -188,6 +202,130 @@ impl SQLParser { Ok(stmt) } + fn try_parse_custom_statement(&self, sql: &str) -> RS> { + let trimmed = sql.trim(); + if trimmed.is_empty() { + return Ok(Some(StmtList::new(Vec::new()))); + } + let normalized = trimmed.trim_end_matches(';').trim(); + if normalized.is_empty() { + return Ok(Some(StmtList::new(Vec::new()))); + } + + if starts_with_ignore_ascii_case(normalized, "create partition rule ") { + let stmt = self.parse_create_partition_rule_custom(normalized)?; + return Ok(Some(StmtList::new(vec![StmtType::Command( + StmtCommand::CreatePartitionRule(stmt), + )]))); + } + + if starts_with_ignore_ascii_case(normalized, "create partition placement ") { + let stmt = self.parse_create_partition_placement_custom(normalized)?; + return Ok(Some(StmtList::new(vec![StmtType::Command( + StmtCommand::CreatePartitionPlacement(stmt), + )]))); + } + + if starts_with_ignore_ascii_case(normalized, "create table ") + && contains_ignore_ascii_case(normalized, " partition by global rule ") + { + let stmt = self.parse_create_table_partitioned_custom(normalized)?; + return Ok(Some(StmtList::new(vec![StmtType::Command( + StmtCommand::CreateTable(stmt), + )]))); + } + + Ok(None) + } + + fn parse_create_table_partitioned_custom(&self, sql: &str) -> RS { + let close_index = find_matching_paren(sql, sql.find('(').ok_or_else(|| { + m_error!(EC::ParseErr, "partitioned create table has no column list") + })?)?; + let base_sql = sql[..=close_index].trim(); + let suffix = sql[close_index + 1..].trim(); + + let mut stmt = match self.parse_standard(base_sql)?.stmts().first() { + Some(StmtType::Command(StmtCommand::CreateTable(stmt))) => stmt.clone(), + _ => { + return Err(m_error!( + EC::ParseErr, + "failed to parse base create table statement" + )); + } + }; + let partition = parse_table_partition_suffix(suffix)?; + stmt.set_partition(partition); + Ok(stmt) + } + + fn parse_create_partition_rule_custom(&self, sql: &str) -> RS { + let prefix = "create partition rule "; + let rest = sql[prefix.len()..].trim(); + let range_pos = find_keyword_position(rest, "range").ok_or_else(|| { + m_error!(EC::ParseErr, "create partition rule must contain RANGE") + })?; + let rule_name = rest[..range_pos].trim(); + if rule_name.is_empty() { + return Err(m_error!(EC::ParseErr, "partition rule name is empty")); + } + + let range_body = rest[range_pos + "range".len()..].trim(); + if !range_body.starts_with('(') { + return Err(m_error!( + EC::ParseErr, + "partition rule RANGE clause must be wrapped in parentheses" + )); + } + let close_index = find_matching_paren(range_body, 0)?; + let inner = range_body[1..close_index].trim(); + let defs = split_top_level_csv(inner); + let mut partitions = Vec::with_capacity(defs.len()); + for def in defs { + partitions.push(parse_range_partition_def(def)?); + } + Ok(StmtCreatePartitionRule::new(rule_name.to_string(), partitions)) + } + + fn parse_create_partition_placement_custom( + &self, + sql: &str, + ) -> RS { + let prefix = "create partition placement "; + let rest = sql[prefix.len()..].trim(); + let for_rule_prefix = "for rule "; + if !starts_with_ignore_ascii_case(rest, for_rule_prefix) { + return Err(m_error!( + EC::ParseErr, + "create partition placement must use FOR RULE" + )); + } + let rest = rest[for_rule_prefix.len()..].trim(); + let open_index = rest.find('(').ok_or_else(|| { + m_error!( + EC::ParseErr, + "create partition placement must contain placement list" + ) + })?; + let close_index = find_matching_paren(rest, open_index)?; + let rule_name = rest[..open_index].trim(); + let inner = &rest[open_index + 1..close_index]; + let placements = split_top_level_csv(inner) + .into_iter() + .map(parse_partition_placement_item) + .collect::>>()?; + if rule_name.is_empty() || placements.is_empty() { + return Err(m_error!( + EC::ParseErr, + "invalid create partition placement statement" + )); + } + Ok(StmtCreatePartitionPlacement::new( + rule_name.to_string(), + placements, + )) + } + fn parse_error(&self, context: &ParseContext, node: &Node) -> RS<()> { if node.has_error() { let mut buffer = Vec::new(); @@ -1023,6 +1161,178 @@ impl SQLParser { } } +fn starts_with_ignore_ascii_case(input: &str, prefix: &str) -> bool { + input + .get(..prefix.len()) + .map(|head| head.eq_ignore_ascii_case(prefix)) + .unwrap_or(false) +} + +fn contains_ignore_ascii_case(input: &str, needle: &str) -> bool { + input.to_ascii_lowercase().contains(&needle.to_ascii_lowercase()) +} + +fn find_keyword_position(input: &str, keyword: &str) -> Option { + let lower = input.to_ascii_lowercase(); + lower.find(&keyword.to_ascii_lowercase()) +} + +fn find_matching_paren(input: &str, open_index: usize) -> RS { + let bytes = input.as_bytes(); + let mut depth = 0usize; + let mut in_single_quote = false; + for (index, byte) in bytes.iter().enumerate().skip(open_index) { + match *byte { + b'\'' => in_single_quote = !in_single_quote, + b'(' if !in_single_quote => depth += 1, + b')' if !in_single_quote => { + depth = depth.saturating_sub(1); + if depth == 0 { + return Ok(index); + } + } + _ => {} + } + } + Err(m_error!(EC::ParseErr, "unbalanced parentheses")) +} + +fn split_top_level_csv(input: &str) -> Vec<&str> { + let mut items = Vec::new(); + let mut start = 0usize; + let mut depth = 0usize; + let mut in_single_quote = false; + for (index, ch) in input.char_indices() { + match ch { + '\'' => in_single_quote = !in_single_quote, + '(' if !in_single_quote => depth += 1, + ')' if !in_single_quote => depth = depth.saturating_sub(1), + ',' if !in_single_quote && depth == 0 => { + items.push(input[start..index].trim()); + start = index + 1; + } + _ => {} + } + } + let tail = input[start..].trim(); + if !tail.is_empty() { + items.push(tail); + } + items +} + +fn parse_table_partition_suffix(input: &str) -> RS { + let prefix = "partition by global rule "; + if !starts_with_ignore_ascii_case(input, prefix) { + return Err(m_error!( + EC::ParseErr, + "expected PARTITION BY GLOBAL RULE clause" + )); + } + let rest = input[prefix.len()..].trim(); + let references_pos = find_keyword_position(rest, "references").ok_or_else(|| { + m_error!(EC::ParseErr, "partition clause must contain REFERENCES") + })?; + let rule_name = rest[..references_pos].trim(); + let refs = rest[references_pos + "references".len()..].trim(); + if !refs.starts_with('(') { + return Err(m_error!( + EC::ParseErr, + "REFERENCES clause must be wrapped in parentheses" + )); + } + let close_index = find_matching_paren(refs, 0)?; + let cols = split_top_level_csv(&refs[1..close_index]) + .into_iter() + .map(|col| col.trim().to_string()) + .filter(|col| !col.is_empty()) + .collect::>(); + if rule_name.is_empty() || cols.is_empty() { + return Err(m_error!(EC::ParseErr, "invalid table partition clause")); + } + Ok(StmtTablePartition::new(rule_name.to_string(), cols)) +} + +fn parse_range_partition_def(input: &str) -> RS { + let prefix = "partition "; + if !starts_with_ignore_ascii_case(input, prefix) { + return Err(m_error!( + EC::ParseErr, + format!("invalid partition definition {}", input) + )); + } + let rest = input[prefix.len()..].trim(); + let values_pos = find_keyword_position(rest, "values").ok_or_else(|| { + m_error!(EC::ParseErr, "partition definition must contain VALUES") + })?; + let name = rest[..values_pos].trim(); + let after_values = rest[values_pos + "values".len()..].trim(); + if !starts_with_ignore_ascii_case(after_values, "from") { + return Err(m_error!(EC::ParseErr, "partition definition must contain FROM")); + } + let after_from = after_values["from".len()..].trim(); + let from_close = find_matching_paren(after_from, 0)?; + let start = parse_partition_bound(&after_from[..=from_close])?; + let after_start = after_from[from_close + 1..].trim(); + if !starts_with_ignore_ascii_case(after_start, "to") { + return Err(m_error!(EC::ParseErr, "partition definition must contain TO")); + } + let after_to = after_start["to".len()..].trim(); + let end_close = find_matching_paren(after_to, 0)?; + let end = parse_partition_bound(&after_to[..=end_close])?; + Ok(StmtRangePartition::new(name.to_string(), start, end)) +} + +fn parse_partition_bound(input: &str) -> RS { + let trimmed = input.trim(); + if !trimmed.starts_with('(') || !trimmed.ends_with(')') { + return Err(m_error!(EC::ParseErr, "partition bound must be parenthesized")); + } + let items = split_top_level_csv(&trimmed[1..trimmed.len() - 1]); + if items.len() == 1 + && (items[0].eq_ignore_ascii_case("minvalue") || items[0].eq_ignore_ascii_case("maxvalue")) + { + return Ok(StmtPartitionBound::Unbounded); + } + let mut values = Vec::with_capacity(items.len()); + for item in items { + if item.eq_ignore_ascii_case("minvalue") || item.eq_ignore_ascii_case("maxvalue") { + return Ok(StmtPartitionBound::Unbounded); + } + values.push(item.trim().as_bytes().to_vec()); + } + Ok(StmtPartitionBound::Value(values)) +} + +fn parse_partition_placement_item(input: &str) -> RS { + let prefix = "partition "; + if !starts_with_ignore_ascii_case(input, prefix) { + return Err(m_error!( + EC::ParseErr, + format!("invalid partition placement item {}", input) + )); + } + let rest = input[prefix.len()..].trim(); + let on_worker = find_keyword_position(rest, "on worker").ok_or_else(|| { + m_error!( + EC::ParseErr, + "partition placement item must contain ON WORKER" + ) + })?; + let partition_name = rest[..on_worker].trim(); + let worker_id = rest[on_worker + "on worker".len()..].trim(); + if partition_name.is_empty() || worker_id.is_empty() { + return Err(m_error!( + EC::ParseErr, + format!("invalid partition placement item {}", input) + )); + } + Ok(StmtPartitionPlacementItem::new( + partition_name.to_string(), + worker_id.to_string(), + )) +} + fn ts_node_context_string(s: &str, n: &Node) -> RS { let ret = s.substring(n.start_byte(), n.end_byte()); Ok(ret.to_string()) diff --git a/sql_parser/src/ast/parser_test.rs b/sql_parser/src/ast/parser_test.rs index 65dbecb..c99d19a 100644 --- a/sql_parser/src/ast/parser_test.rs +++ b/sql_parser/src/ast/parser_test.rs @@ -71,6 +71,24 @@ mod tests { } } + #[test] + fn parse_select_reverts_literal_field_comparison_shape() { + let stmts = parse_sql("select id from users where 7 > id;").unwrap(); + + let StmtType::Select(stmt) = &stmts[0] else { + panic!("expected select"); + }; + let predicate = stmt.get_where_predicate()[0] + .expr_field_op_literal() + .expect("expected field-literal pair"); + assert_eq!(predicate.0.name(), "id"); + assert_eq!( + predicate.1.dat_type().dat_type().dat_type_id(), + mudu_type::dat_type_id::DatTypeID::I64 + ); + assert!(matches!(predicate.2, ValueCompare::LE)); + } + #[test] fn parse_insert_without_column_list() { let stmts = parse_sql("insert into users values (1, 'alice');").unwrap(); @@ -96,6 +114,15 @@ mod tests { assert_eq!(stmt.values_list().len(), 2); } + #[test] + fn parse_multiple_statements_with_trailing_semicolons() { + let stmts = parse_sql("insert into users values (1); delete from users where id = 1;") + .unwrap(); + assert_eq!(stmts.len(), 2); + assert!(matches!(stmts[0], StmtType::Command(StmtCommand::Insert(_)))); + assert!(matches!(stmts[1], StmtType::Command(StmtCommand::Delete(_)))); + } + #[test] fn parse_update_distinguishes_value_and_expression_assignments() { let stmts = @@ -118,6 +145,27 @@ mod tests { assert_eq!(stmt.get_where_predicate().len(), 1); } + #[test] + fn parse_update_keeps_arithmetic_precedence_shape() { + let stmts = parse_sql("update users set total = count + 1 * 2 where id = 1;").unwrap(); + + let StmtType::Command(StmtCommand::Update(stmt)) = &stmts[0] else { + panic!("expected update"); + }; + match stmt.get_set_values()[0].get_set_value() { + AssignedValue::Expression(ExprType::Arithmetic(expr)) => { + assert!(matches!(expr.op(), Arithmetic::PLUS)); + match expr.right() { + ExprType::Arithmetic(nested) => { + assert!(matches!(nested.op(), Arithmetic::MULTIPLE)); + } + other => panic!("expected nested multiply, got {other:?}"), + } + } + other => panic!("expected arithmetic assignment, got {other:?}"), + } + } + #[test] fn parse_delete_with_and_predicates() { let stmts = parse_sql("delete from users where id = 1 AND name = 'alice';").unwrap(); @@ -296,4 +344,68 @@ mod tests { let r = parse_file(path); assert!(r.is_ok()) } + + #[test] + fn parse_create_partition_rule_custom_statement() { + let stmts = parse_sql( + " + CREATE PARTITION RULE r_orders RANGE ( + PARTITION p0 VALUES FROM (MINVALUE, MINVALUE) TO (1000, MINVALUE), + PARTITION p1 VALUES FROM (1000, MINVALUE) TO (MAXVALUE, MAXVALUE) + ); + ", + ) + .unwrap(); + + let StmtType::Command(StmtCommand::CreatePartitionRule(stmt)) = &stmts[0] else { + panic!("expected create partition rule"); + }; + assert_eq!(stmt.rule_name(), "r_orders"); + assert_eq!(stmt.partitions().len(), 2); + assert_eq!(stmt.partitions()[0].name(), "p0"); + } + + #[test] + fn parse_create_table_with_partition_binding_clause() { + let stmt = parse_create_table( + " + CREATE TABLE orders ( + region_id INT, + order_id INT, + amount INT, + PRIMARY KEY (region_id, order_id) + ) + PARTITION BY GLOBAL RULE r_orders REFERENCES (region_id, order_id); + ", + ) + .unwrap(); + + let partition = stmt.partition().expect("expected partition binding"); + assert_eq!(partition.rule_name(), "r_orders"); + assert_eq!( + partition.reference_columns(), + &vec!["region_id".to_string(), "order_id".to_string()] + ); + } + + #[test] + fn parse_create_partition_placement_custom_statement() { + let stmts = parse_sql( + " + CREATE PARTITION PLACEMENT FOR RULE r_orders ( + PARTITION p0 ON WORKER 11, + PARTITION p1 ON WORKER 12 + ); + ", + ) + .unwrap(); + + let StmtType::Command(StmtCommand::CreatePartitionPlacement(stmt)) = &stmts[0] else { + panic!("expected create partition placement"); + }; + assert_eq!(stmt.rule_name(), "r_orders"); + assert_eq!(stmt.placements().len(), 2); + assert_eq!(stmt.placements()[0].partition_name(), "p0"); + assert_eq!(stmt.placements()[0].worker_id(), "11"); + } } diff --git a/sql_parser/src/ast/stmt_copy_to.rs b/sql_parser/src/ast/stmt_copy_to.rs index 1fba876..c8e894e 100644 --- a/sql_parser/src/ast/stmt_copy_to.rs +++ b/sql_parser/src/ast/stmt_copy_to.rs @@ -33,3 +33,21 @@ impl StmtCopyTo { impl ASTNode for StmtCopyTo {} impl StmtCopyTo {} + +#[cfg(test)] +mod tests { + use super::StmtCopyTo; + + #[test] + fn copy_to_accessors_return_constructor_values() { + let stmt = StmtCopyTo::new( + "'users.csv'".to_string(), + "users".to_string(), + vec!["id".to_string(), "name".to_string()], + ); + + assert_eq!(stmt.copy_to_file_path(), "'users.csv'"); + assert_eq!(stmt.copy_from_table_name(), "users"); + assert_eq!(stmt.table_columns(), &vec!["id".to_string(), "name".to_string()]); + } +} diff --git a/sql_parser/src/ast/stmt_create_partition_placement.rs b/sql_parser/src/ast/stmt_create_partition_placement.rs new file mode 100644 index 0000000..769b366 --- /dev/null +++ b/sql_parser/src/ast/stmt_create_partition_placement.rs @@ -0,0 +1,49 @@ +use crate::ast::ast_node::ASTNode; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StmtPartitionPlacementItem { + partition_name: String, + worker_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StmtCreatePartitionPlacement { + rule_name: String, + placements: Vec, +} + +impl StmtPartitionPlacementItem { + pub fn new(partition_name: String, worker_id: String) -> Self { + Self { + partition_name, + worker_id, + } + } + + pub fn partition_name(&self) -> &str { + &self.partition_name + } + + pub fn worker_id(&self) -> &str { + &self.worker_id + } +} + +impl StmtCreatePartitionPlacement { + pub fn new(rule_name: String, placements: Vec) -> Self { + Self { + rule_name, + placements, + } + } + + pub fn rule_name(&self) -> &str { + &self.rule_name + } + + pub fn placements(&self) -> &[StmtPartitionPlacementItem] { + &self.placements + } +} + +impl ASTNode for StmtCreatePartitionPlacement {} diff --git a/sql_parser/src/ast/stmt_create_partition_rule.rs b/sql_parser/src/ast/stmt_create_partition_rule.rs new file mode 100644 index 0000000..0e10eb7 --- /dev/null +++ b/sql_parser/src/ast/stmt_create_partition_rule.rs @@ -0,0 +1,57 @@ +use crate::ast::ast_node::ASTNode; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum StmtPartitionBound { + Unbounded, + Value(Vec>), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StmtRangePartition { + name: String, + start: StmtPartitionBound, + end: StmtPartitionBound, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StmtCreatePartitionRule { + rule_name: String, + partitions: Vec, +} + +impl StmtRangePartition { + pub fn new(name: String, start: StmtPartitionBound, end: StmtPartitionBound) -> Self { + Self { name, start, end } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn start(&self) -> &StmtPartitionBound { + &self.start + } + + pub fn end(&self) -> &StmtPartitionBound { + &self.end + } +} + +impl StmtCreatePartitionRule { + pub fn new(rule_name: String, partitions: Vec) -> Self { + Self { + rule_name, + partitions, + } + } + + pub fn rule_name(&self) -> &str { + &self.rule_name + } + + pub fn partitions(&self) -> &[StmtRangePartition] { + &self.partitions + } +} + +impl ASTNode for StmtCreatePartitionRule {} diff --git a/sql_parser/src/ast/stmt_create_table.rs b/sql_parser/src/ast/stmt_create_table.rs index c52cb7d..a38c323 100644 --- a/sql_parser/src/ast/stmt_create_table.rs +++ b/sql_parser/src/ast/stmt_create_table.rs @@ -1,5 +1,6 @@ use crate::ast::ast_node::ASTNode; use crate::ast::column_def::ColumnDef; +use crate::ast::stmt_table_partition::StmtTablePartition; use mudu::common::id::AttrIndex; use std::fmt::Debug; @@ -9,6 +10,7 @@ pub struct StmtCreateTable { column_def: Vec, primary_key_column_def: Vec, non_primary_key_column_def: Vec, + partition: Option, } impl StmtCreateTable { @@ -18,6 +20,7 @@ impl StmtCreateTable { column_def: vec![], primary_key_column_def: vec![], non_primary_key_column_def: vec![], + partition: None, } } @@ -65,6 +68,14 @@ impl StmtCreateTable { .collect() } + pub fn partition(&self) -> Option<&StmtTablePartition> { + self.partition.as_ref() + } + + pub fn set_partition(&mut self, partition: StmtTablePartition) { + self.partition = Some(partition); + } + pub fn assign_index_for_columns(&mut self) { self.primary_key_column_def.clear(); self.non_primary_key_column_def.clear(); diff --git a/sql_parser/src/ast/stmt_table_partition.rs b/sql_parser/src/ast/stmt_table_partition.rs new file mode 100644 index 0000000..d95fa4d --- /dev/null +++ b/sql_parser/src/ast/stmt_table_partition.rs @@ -0,0 +1,26 @@ +use crate::ast::ast_node::ASTNode; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StmtTablePartition { + rule_name: String, + reference_columns: Vec, +} + +impl StmtTablePartition { + pub fn new(rule_name: String, reference_columns: Vec) -> Self { + Self { + rule_name, + reference_columns, + } + } + + pub fn rule_name(&self) -> &str { + &self.rule_name + } + + pub fn reference_columns(&self) -> &[String] { + &self.reference_columns + } +} + +impl ASTNode for StmtTablePartition {} diff --git a/sql_parser/src/ast/stmt_type.rs b/sql_parser/src/ast/stmt_type.rs index 2eb6bd9..11126f9 100644 --- a/sql_parser/src/ast/stmt_type.rs +++ b/sql_parser/src/ast/stmt_type.rs @@ -1,5 +1,7 @@ use crate::ast::stmt_copy_from::StmtCopyFrom; use crate::ast::stmt_copy_to::StmtCopyTo; +use crate::ast::stmt_create_partition_placement::StmtCreatePartitionPlacement; +use crate::ast::stmt_create_partition_rule::StmtCreatePartitionRule; use crate::ast::stmt_create_table::StmtCreateTable; use crate::ast::stmt_delete::StmtDelete; use crate::ast::stmt_drop_table::StmtDropTable; @@ -17,7 +19,9 @@ pub enum StmtType { pub enum StmtCommand { Update(StmtUpdate), Delete(StmtDelete), + CreatePartitionPlacement(StmtCreatePartitionPlacement), Insert(StmtInsert), + CreatePartitionRule(StmtCreatePartitionRule), CreateTable(StmtCreateTable), DropTable(StmtDropTable), CopyTo(StmtCopyTo), diff --git a/sql_parser/src/ast/type_declare.rs b/sql_parser/src/ast/type_declare.rs index eab79d3..96f7608 100644 --- a/sql_parser/src/ast/type_declare.rs +++ b/sql_parser/src/ast/type_declare.rs @@ -28,3 +28,21 @@ impl TypeDeclare { self.param.to_info() } } + +#[cfg(test)] +mod tests { + use super::TypeDeclare; + use mudu_type::dat_type::DatType; + use mudu_type::dat_type_id::DatTypeID; + + #[test] + fn type_declare_exposes_param_metadata() { + let ty = DatType::new_no_param(DatTypeID::I64); + let declare = TypeDeclare::new(ty.clone()); + + assert_eq!(declare.id(), DatTypeID::I64); + assert_eq!(declare.param().dat_type_id(), DatTypeID::I64); + assert_eq!(declare.param_info().id, ty.to_info().id); + assert_eq!(declare.param_info().param, ty.to_info().param); + } +} diff --git a/sys_interface/Cargo.toml b/sys_interface/Cargo.toml index 380c730..271658b 100644 --- a/sys_interface/Cargo.toml +++ b/sys_interface/Cargo.toml @@ -23,3 +23,8 @@ mudu_adapter = { workspace = true, optional = true } uniffi = { workspace = true, features = ["tokio"], optional = true } thiserror = { version = "2.0.17", optional = true } tokio = { workspace = true, optional = true } + +[dev-dependencies] +mudu_adapter = { workspace = true } +mudu_type = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread"] } diff --git a/sys_interface/src/api_impl/mod.rs b/sys_interface/src/api_impl/mod.rs index cc5112a..2df4887 100644 --- a/sys_interface/src/api_impl/mod.rs +++ b/sys_interface/src/api_impl/mod.rs @@ -75,3 +75,50 @@ pub(crate) fn fetch_cursor_oid(cursor: &[u8]) -> RS { let (oid, _) = deserialize_from::(cursor)?; Ok(oid.to_oid()) } + +#[cfg(test)] +mod tests { + use super::{fetch_cursor_oid, result_batch_to_uni, serialize_fetch_result}; + use mudu::common::serde_utils::{deserialize_from, serialize_to_vec}; + use mudu_binding::universal::uni_error::UniError; + use mudu_binding::universal::uni_oid::UniOid; + use mudu_binding::universal::uni_result::UniResult; + use mudu_binding::universal::uni_result_set::UniResultSet; + use mudu_contract::database::result_batch::ResultBatch; + use mudu_contract::tuple::tuple_value::TupleValue; + use mudu_type::dat_value::DatValue; + + #[test] + fn result_batch_helpers_roundtrip_cursor_and_rows() { + let batch = ResultBatch::from(7, vec![TupleValue::from(vec![DatValue::from_i32(11)])], true); + let uni = result_batch_to_uni(batch).unwrap(); + + assert!(uni.eof); + assert_eq!(fetch_cursor_oid(&uni.cursor).unwrap(), 7); + assert_eq!(uni.row_set.len(), 1); + } + + #[test] + fn serialize_fetch_result_encodes_ok_and_err_payloads() { + let ok = serialize_fetch_result(Ok(ResultBatch::from(9, Vec::new(), true))).unwrap(); + let payload: UniResult = deserialize_from(&ok).unwrap().0; + match payload { + UniResult::Ok(result_set) => assert_eq!(fetch_cursor_oid(&result_set.cursor).unwrap(), 9), + UniResult::Err(err) => panic!("unexpected error payload: {}", err.err_msg), + } + + let err = serialize_fetch_result(Err(mudu::m_error!(mudu::error::ec::EC::ParseErr, "boom"))) + .unwrap(); + let payload: UniResult = deserialize_from(&err).unwrap().0; + match payload { + UniResult::Ok(_) => panic!("expected error payload"), + UniResult::Err(err) => assert_eq!(err.err_msg, "boom"), + } + } + + #[test] + fn fetch_cursor_oid_decodes_universal_oid_binary() { + let cursor = serialize_to_vec(&UniOid::from(42_u128)).unwrap(); + assert_eq!(fetch_cursor_oid(&cursor).unwrap(), 42); + } +} diff --git a/sys_interface/src/host.rs b/sys_interface/src/host.rs index 58caaae..5b77e4b 100644 --- a/sys_interface/src/host.rs +++ b/sys_interface/src/host.rs @@ -421,6 +421,12 @@ impl ResultSet for ResultSetWrapper { #[cfg(test)] mod tests { use super::*; + use mudu_binding::system::{command_invoke, query_invoke}; + use mudu_binding::universal::uni_session_open_argv::UniSessionOpenArgv; + use mudu_contract::database::sql_stmt_text::SQLStmtText; + use mudu_contract::tuple::tuple_datum::TupleDatum; + use mudu_contract::tuple::tuple_value::TupleValue; + use mudu_type::dat_value::DatValue; #[test] fn kv_get_roundtrip() { @@ -452,4 +458,93 @@ mod tests { ] ); } + + #[test] + fn open_and_open_argv_helpers_roundtrip() { + let oid = invoke_host_open(|_| Ok(serialize_open_result(15))).unwrap(); + assert_eq!(oid, 15); + + let argv = UniSessionOpenArgv::new(7); + let oid = invoke_host_open_argv(&argv, |input| { + let decoded = deserialize_open_param(&input).unwrap(); + assert_eq!(decoded.worker_oid(), 7); + Ok(serialize_open_result(21)) + }) + .unwrap(); + assert_eq!(oid, 21); + } + + #[test] + fn command_and_query_helpers_decode_serialized_results() { + let stmt = SQLStmtText::new("SELECT 1".to_string()); + + let affected = invoke_host_command(3, &stmt, &(), |input| { + let (oid, _, _) = command_invoke::deserialize_command_param(&input).unwrap(); + assert_eq!(oid, 3); + Ok(command_invoke::serialize_command_result(Ok(5))) + }) + .unwrap(); + assert_eq!(affected, 5); + + let records = invoke_host_query::(4, &stmt, &(), |input| { + let (oid, _, _) = query_invoke::deserialize_query_param(&input).unwrap(); + assert_eq!(oid, 4); + Ok(query_invoke::serialize_query_result(Ok(( + mudu_contract::database::result_batch::ResultBatch::from( + 4, + vec![TupleValue::from(vec![DatValue::from_i32(8)])], + true, + ), + ::tuple_desc_static(&["value".to_string()]), + )))) + }) + .unwrap(); + assert_eq!(records.next_record().unwrap(), Some(8)); + } + + #[tokio::test] + async fn async_host_helpers_roundtrip_sync_payload_shapes() { + let stmt = SQLStmtText::new("SELECT 1".to_string()); + + let oid = async_invoke_host_open(|_| async { Ok(serialize_open_result(31)) }) + .await + .unwrap(); + assert_eq!(oid, 31); + + let affected = async_invoke_host_batch(6, &stmt, &(), |input: Vec| async move { + let (oid, _, _) = command_invoke::deserialize_command_param(&input).unwrap(); + assert_eq!(oid, 6); + Ok(command_invoke::serialize_command_result(Ok(2))) + }) + .await + .unwrap(); + assert_eq!(affected, 2); + + let records = + async_invoke_host_query::(8, &stmt, &(), |input: Vec| async move { + let (oid, _, _) = query_invoke::deserialize_query_param(&input).unwrap(); + assert_eq!(oid, 8); + Ok(query_invoke::serialize_query_result(Ok(( + mudu_contract::database::result_batch::ResultBatch::from( + 8, + vec![TupleValue::from(vec![DatValue::from_i32(13)])], + true, + ), + ::tuple_desc_static(&["value".to_string()]), + )))) + }) + .await + .unwrap(); + assert_eq!(records.next_record().unwrap(), Some(13)); + + let got = async_invoke_host_session_get(9, b"k", |input: Vec| async move { + let (oid, key) = deserialize_session_get_param(&input).unwrap(); + assert_eq!(oid, 9); + assert_eq!(key, b"k"); + Ok(serialize_get_result(Some(b"v"))) + }) + .await + .unwrap(); + assert_eq!(got, Some(b"v".to_vec())); + } } diff --git a/sys_interface/standalone_adapter.md b/sys_interface/standalone_adapter.md index 0040555..a024e7f 100644 --- a/sys_interface/standalone_adapter.md +++ b/sys_interface/standalone_adapter.md @@ -47,9 +47,9 @@ Supported forms: - `postgres://user:pass@127.0.0.1:5432/app_db` - `postgresql://user:pass@127.0.0.1:5432/app_db` - `mysql://user:pass@127.0.0.1:3306/app_db` -- `mudud://127.0.0.1:9000/app_name` -- `mudud://127.0.0.1:9000/app_name?http_addr=127.0.0.1:8300` -- `mudud://127.0.0.1:9000/app_name?http_addr=127.0.0.1:8300&async_session_loop=true` +- `mudud://127.0.0.1:9527/app_name` +- `mudud://127.0.0.1:9527/app_name?http_addr=127.0.0.1:8300` +- `mudud://127.0.0.1:9527/app_name?http_addr=127.0.0.1:8300&async_session_loop=true` If `MUDU_CONNECTION` is not set, the default is: @@ -126,7 +126,7 @@ Notes: Example: ```bash -export MUDU_CONNECTION="mudud://127.0.0.1:9000/app1?http_addr=127.0.0.1:8300" +export MUDU_CONNECTION="mudud://127.0.0.1:9527/app1?http_addr=127.0.0.1:8300" cargo run ``` diff --git a/sys_interface/tests/standalone_api_test.rs b/sys_interface/tests/standalone_api_test.rs new file mode 100644 index 0000000..c816a92 --- /dev/null +++ b/sys_interface/tests/standalone_api_test.rs @@ -0,0 +1,219 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "standalone-adapter"))] + +use mudu_contract::database::sql_stmt_text::SQLStmtText; +use std::path::PathBuf; +use std::sync::{Mutex, OnceLock}; +use std::time::{SystemTime, UNIX_EPOCH}; +use sys_interface::{async_api, host, sync_api}; + +fn test_lock() -> &'static Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) +} + +fn lock_tests() -> std::sync::MutexGuard<'static, ()> { + test_lock().lock().unwrap_or_else(|err| err.into_inner()) +} + +fn temp_db_path(name: &str) -> PathBuf { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!("sys_interface_{name}_{suffix}.db")) +} + +#[test] +fn sync_standalone_kv_and_sql_wrappers_work() { + let _guard = lock_tests(); + let db_path = temp_db_path("sync"); + mudu_adapter::config::reset_db_path_override_for_test(); + mudu_adapter::syscall::set_db_path(&db_path); + + let session_id = sync_api::mudu_open().unwrap(); + sync_api::mudu_put(session_id, b"k2", b"v2").unwrap(); + sync_api::mudu_put(session_id, b"k1", b"v1").unwrap(); + + assert_eq!( + sync_api::mudu_get(session_id, b"k1").unwrap(), + Some(b"v1".to_vec()) + ); + assert_eq!( + sync_api::mudu_range(session_id, b"k1", b"").unwrap(), + vec![ + (b"k1".to_vec(), b"v1".to_vec()), + (b"k2".to_vec(), b"v2".to_vec()), + ] + ); + + let setup = SQLStmtText::new( + "CREATE TABLE demo(id INT PRIMARY KEY); INSERT INTO demo(id) VALUES (7);".to_string(), + ); + assert_eq!( + sync_api::mudu_batch(session_id, &setup, &()).unwrap(), + 1 + ); + + let insert = SQLStmtText::new("INSERT INTO demo(id) VALUES (?1)".to_string()); + assert_eq!( + sync_api::mudu_command(session_id, &insert, &(9_i32,)).unwrap(), + 1 + ); + + let query = SQLStmtText::new("SELECT id FROM demo WHERE id = ?1".to_string()); + let rows = sync_api::mudu_query::(session_id, &query, &(9_i32,)).unwrap(); + assert_eq!(rows.next_record().unwrap(), Some(9)); + assert_eq!(rows.next_record().unwrap(), None); + + sync_api::mudu_close(session_id).unwrap(); +} + +#[test] +fn sync_bytes_kv_flow_roundtrips() { + let _guard = lock_tests(); + let db_path = temp_db_path("sync_bytes"); + mudu_adapter::config::reset_db_path_override_for_test(); + mudu_adapter::syscall::set_db_path(&db_path); + + let open_out = sync_api::mudu_open_bytes(&host::serialize_open_param()).unwrap(); + let session_id = host::deserialize_open_result(&open_out).unwrap(); + + let put_in = host::serialize_session_put_param(session_id, b"alpha", b"beta"); + let put_out = sync_api::mudu_put_bytes(&put_in).unwrap(); + host::deserialize_put_result(&put_out).unwrap(); + + let get_in = host::serialize_session_get_param(session_id, b"alpha"); + let get_out = sync_api::mudu_get_bytes(&get_in).unwrap(); + assert_eq!( + host::deserialize_get_result(&get_out).unwrap(), + Some(b"beta".to_vec()) + ); + + let range_in = host::serialize_session_range_param(session_id, b"a", b"z"); + let range_out = sync_api::mudu_range_bytes(&range_in).unwrap(); + assert_eq!( + host::deserialize_range_result(&range_out).unwrap(), + vec![(b"alpha".to_vec(), b"beta".to_vec())] + ); + + let close_out = sync_api::mudu_close_bytes(&host::serialize_close_param(session_id)).unwrap(); + host::deserialize_close_result(&close_out).unwrap(); +} + +#[test] +fn host_invoke_helpers_roundtrip_through_sync_bytes_handlers() { + let _guard = lock_tests(); + let db_path = temp_db_path("host_helpers"); + mudu_adapter::config::reset_db_path_override_for_test(); + mudu_adapter::syscall::set_db_path(&db_path); + + let session_id = host::invoke_host_open(|input| sync_api::mudu_open_bytes(&input)).unwrap(); + host::invoke_host_session_put(session_id, b"key", b"value", |input| { + sync_api::mudu_put_bytes(&input) + }) + .unwrap(); + assert_eq!( + host::invoke_host_session_get(session_id, b"key", |input| sync_api::mudu_get_bytes(&input)) + .unwrap(), + Some(b"value".to_vec()) + ); + assert_eq!( + host::invoke_host_session_range(session_id, b"k", b"z", |input| { + sync_api::mudu_range_bytes(&input) + }) + .unwrap(), + vec![(b"key".to_vec(), b"value".to_vec())] + ); + host::invoke_host_close(session_id, |input| sync_api::mudu_close_bytes(&input)).unwrap(); +} + +#[test] +fn async_standalone_kv_and_sql_wrappers_work() { + let _guard = lock_tests(); + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + let db_path = temp_db_path("async"); + mudu_adapter::config::reset_db_path_override_for_test(); + mudu_adapter::syscall::set_db_path(&db_path); + + let session_id = async_api::mudu_open().await.unwrap(); + async_api::mudu_put(session_id, b"k1", b"v1") + .await + .unwrap(); + assert_eq!( + async_api::mudu_get(session_id, b"k1") + .await + .unwrap(), + Some(b"v1".to_vec()) + ); + assert_eq!( + async_api::mudu_range(session_id, b"k1", b"") + .await + .unwrap(), + vec![(b"k1".to_vec(), b"v1".to_vec())] + ); + + let setup = SQLStmtText::new( + "CREATE TABLE demo(id INT PRIMARY KEY); INSERT INTO demo(id) VALUES (21);".to_string(), + ); + assert_eq!( + async_api::mudu_batch(session_id, &setup, &()) + .await + .unwrap(), + 1 + ); + + let query = SQLStmtText::new("SELECT id FROM demo WHERE id = ?1".to_string()); + let rows = async_api::mudu_query::(session_id, &query, &(21_i32,)) + .await + .unwrap(); + assert_eq!(rows.next_record().unwrap(), Some(21)); + + async_api::mudu_close(session_id).await.unwrap(); + }); +} + +#[test] +fn async_bytes_kv_flow_roundtrips() { + let _guard = lock_tests(); + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + let db_path = temp_db_path("async_bytes"); + mudu_adapter::config::reset_db_path_override_for_test(); + mudu_adapter::syscall::set_db_path(&db_path); + + let open_out = async_api::mudu_open_bytes(&host::serialize_open_param()) + .await + .unwrap(); + let session_id = host::deserialize_open_result(&open_out).unwrap(); + + let put_in = host::serialize_session_put_param(session_id, b"left", b"right"); + let put_out = async_api::mudu_put_bytes(&put_in).await.unwrap(); + host::deserialize_put_result(&put_out).unwrap(); + + let get_out = async_api::mudu_get_bytes( + &host::serialize_session_get_param(session_id, b"left"), + ) + .await + .unwrap(); + assert_eq!( + host::deserialize_get_result(&get_out).unwrap(), + Some(b"right".to_vec()) + ); + + let range_out = async_api::mudu_range_bytes( + &host::serialize_session_range_param(session_id, b"l", b"z"), + ) + .await + .unwrap(); + assert_eq!( + host::deserialize_range_result(&range_out).unwrap(), + vec![(b"left".to_vec(), b"right".to_vec())] + ); + + let close_out = async_api::mudu_close_bytes(&host::serialize_close_param(session_id)) + .await + .unwrap(); + host::deserialize_close_result(&close_out).unwrap(); + }); +} diff --git a/testing/Cargo.toml b/testing/Cargo.toml new file mode 100644 index 0000000..d40492a --- /dev/null +++ b/testing/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "testing" +version = "0.1.0" +edition = "2024" + +[dependencies] +mudu_runtime = { workspace = true } +mudu_utils = { workspace = true } +mudu_cli = { workspace = true } +mudu_binding = { workspace = true } +mudu_contract = { workspace = true } +mudu = { workspace = true } +mudu_sys = { workspace = true } +tokio = { workspace = true } +reqwest = { workspace = true } +serde_json = { workspace = true } +base64 = { workspace = true } +libsql = { version = "0.9.24" } +tracing = { workspace = true } diff --git a/testing/Makefile.toml b/testing/Makefile.toml new file mode 100644 index 0000000..3ddb8ec --- /dev/null +++ b/testing/Makefile.toml @@ -0,0 +1,27 @@ +[env] +WALLET_DIR = "${CARGO_MAKE_WORKING_DIRECTORY}/../example/wallet" +WALLET_MPK = "${CARGO_MAKE_WORKING_DIRECTORY}/../target/wasm32-wasip2/release/wallet.mpk" +TESTING_MPK_DIR = "${CARGO_MAKE_WORKING_DIRECTORY}/mpk" +TESTING_WALLET_MPK = "${TESTING_MPK_DIR}/wallet.mpk" + +[tasks.build-wallet-mpk] +command = "cargo" +args = ["make", "package"] +cwd = "${WALLET_DIR}" + +[tasks.prepare-mpk-dir] +command = "mkdir" +args = ["-p", "${TESTING_MPK_DIR}"] + +[tasks.copy-wallet-mpk] +dependencies = ["build-wallet-mpk", "prepare-mpk-dir"] +command = "cp" +args = ["${WALLET_MPK}", "${TESTING_WALLET_MPK}"] + +[tasks.test] +dependencies = ["copy-wallet-mpk"] +command = "cargo" +args = ["test", "-p", "testing", "--", "--nocapture"] + +[tasks.default] +alias = "test" diff --git a/testing/mpk/wallet.mpk b/testing/mpk/wallet.mpk new file mode 100644 index 0000000000000000000000000000000000000000..eabf87af961f826b34b63b2bdf22e12c651256d8 GIT binary patch literal 1108807 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 z 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%0A!mZwdJP6{2;YL7I`Q(UDi+s&V)#WLxH-M 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 literal 0 HcmV?d00001 diff --git a/testing/src/lib.rs b/testing/src/lib.rs new file mode 100644 index 0000000..a72506e --- /dev/null +++ b/testing/src/lib.rs @@ -0,0 +1,57 @@ +use mudu::common::result::RS; +use mudu_cli::client::client::SyncClient; +use std::net::{TcpListener, TcpStream}; +use std::time::Duration; + +pub fn reserve_port() -> RS> { + match TcpListener::bind("127.0.0.1:0") { + Ok(listener) => Ok(Some( + listener + .local_addr() + .map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::NetErr, "read local addr error", e) + })? + .port(), + )), + Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => Ok(None), + Err(e) => Err(mudu::m_error!( + mudu::error::ec::EC::NetErr, + "reserve local tcp port error", + e + )), + } +} + +pub fn wait_until_port_ready(port: u16, service_name: &str) -> RS<()> { + let deadline = mudu_sys::time::instant_now() + Duration::from_secs(10); + while mudu_sys::time::instant_now() < deadline { + if TcpStream::connect(("127.0.0.1", port)).is_ok() { + return Ok(()); + } + mudu_sys::task::sleep_blocking(Duration::from_millis(25)); + } + Err(mudu::m_error!( + mudu::error::ec::EC::NetErr, + format!("{} server did not become ready on port {}", service_name, port) + )) +} + +pub fn connect_sync_client_with_retry(port: u16) -> RS { + let deadline = mudu_sys::time::instant_now() + Duration::from_secs(5); + let mut last_err = None; + while mudu_sys::time::instant_now() < deadline { + match SyncClient::connect(("127.0.0.1", port)) { + Ok(client) => return Ok(client), + Err(err) => { + last_err = Some(err); + mudu_sys::task::sleep_blocking(Duration::from_millis(50)); + } + } + } + Err(last_err.unwrap_or_else(|| { + mudu::m_error!( + mudu::error::ec::EC::NetErr, + format!("timed out connecting SyncClient to TCP port {}", port) + ) + })) +} diff --git a/testing/tests/wallet_mpk.rs b/testing/tests/wallet_mpk.rs new file mode 100644 index 0000000..1990807 --- /dev/null +++ b/testing/tests/wallet_mpk.rs @@ -0,0 +1,563 @@ +#![cfg(target_os = "linux")] + +use base64::Engine; +use libsql::{params, Builder, Value as LibsqlValue}; +use mudu::common::result::RS; +use mudu_binding::procedure::procedure_invoke; +use mudu_cli::client::async_client::{AsyncClient, AsyncClientImpl}; +use mudu_cli::management::{fetch_server_topology, install_app_package}; +use mudu_contract::procedure::procedure_param::ProcedureParam; +use mudu_contract::tuple::tuple_datum::TupleDatum; +use mudu_runtime::backend::backend::Backend; +use mudu_runtime::backend::mududb_cfg::{MuduDBCfg, RoutingMode, ServerMode}; +use mudu_runtime::service::runtime_opt::ComponentTarget; +use mudu_utils::notifier::{notify_wait, Notifier}; +use serde_json::{json, Value}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::thread::{self, JoinHandle}; +use testing::{reserve_port, wait_until_port_ready}; + +#[test] +fn wallet_mpk_http_end_to_end() -> RS<()> { + let Some(ctx) = TestContext::new(ServerMode::Legacy)? else { + eprintln!("skip wallet HTTP io_uring test: local TCP/HTTP bind is not permitted"); + return Ok(()); + }; + let _server = ctx.start_server()?; + + let mpk_binary = fs::read(ctx.wallet_mpk_path()).expect("read wallet.mpk"); + let install_response = ctx.post_json( + "/mudu/app/install", + json!({ + "mpk_base64": base64::engine::general_purpose::STANDARD.encode(mpk_binary), + }), + )?; + assert_eq!(install_response, Value::Null); + + let apps = ctx.get_json("/mudu/app/list")?; + assert_eq!(apps, json!(["wallet"])); + + let procedures = ctx.get_json("/mudu/app/list/wallet")?; + let procedure_list = procedures["procedures"].as_array().expect("procedure list"); + assert!(procedure_list.contains(&json!("wallet/create_user"))); + assert!(procedure_list.contains(&json!("wallet/deposit"))); + assert!(procedure_list.contains(&json!("wallet/transfer_funds"))); + + let detail = ctx.get_json("/mudu/app/list/wallet/wallet/create_user")?; + assert_eq!(detail["proc_desc"]["proc_name"], json!("create_user")); + assert_eq!( + detail["param_default"], + json!({ + "user_id": 0, + "name": "", + "email": "", + }) + ); + + let create_user = ctx.post_json( + "/mudu/app/invoke/wallet/wallet/create_user", + json!({ + "user_id": 3, + "name": "Carol", + "email": "carol@example.com", + }), + )?; + assert_eq!(create_user, json!({ "return_list": [] })); + + let deposit = ctx.post_json( + "/mudu/app/invoke/wallet/wallet/deposit", + json!({ + "user_id": 1, + "amount": 250, + }), + )?; + assert_eq!(deposit, json!({ "return_list": [] })); + + let transfer = ctx.post_json( + "/mudu/app/invoke/wallet/wallet/transfer_funds", + json!({ + "from_user_id": 1, + "to_user_id": 2, + "amount": 500, + }), + )?; + assert_eq!(transfer, json!({ "return_list": [] })); + + assert_eq!( + ctx.query_i64("SELECT COUNT(*) FROM users WHERE user_id = 3")?, + 1 + ); + assert_eq!( + ctx.query_string("SELECT name FROM users WHERE user_id = 3")?, + "Carol" + ); + assert_eq!( + ctx.query_i64("SELECT balance FROM wallets WHERE user_id = 1")?, + 9750 + ); + assert_eq!( + ctx.query_i64("SELECT balance FROM wallets WHERE user_id = 2")?, + 10500 + ); + assert_eq!( + ctx.query_i64( + "SELECT COUNT(*) FROM transactions WHERE trans_type = 'DEPOSIT' AND to_user = 1 AND amount = 250" + )?, + 1 + ); + assert_eq!( + ctx.query_i64( + "SELECT COUNT(*) FROM transactions WHERE from_user = 1 AND to_user = 2 AND amount = 500" + )?, + 1 + ); + + Ok(()) +} + +#[test] +fn wallet_mpk_via_mudu_cli_library() -> RS<()> { + let Some(ctx) = TestContext::new(ServerMode::IOUring)? else { + eprintln!("skip wallet mudu_cli io_uring test: local TCP/HTTP bind is not permitted"); + return Ok(()); + }; + let _server = ctx.start_server()?; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime"); + + let mpk_binary = fs::read(ctx.wallet_mpk_path()).expect("read wallet.mpk"); + runtime + .block_on(install_app_package(&ctx.http_addr(), mpk_binary)) + .map_err(to_mudu_error)?; + + let mut client = runtime + .block_on(AsyncClientImpl::connect(&format!("127.0.0.1:{}", ctx.client_port()))) + .map_err(|e| to_mudu_error(e.to_string()))?; + let topology = runtime + .block_on(fetch_server_topology(&ctx.http_addr())) + .map_err(to_mudu_error)?; + let default_worker_id = topology + .workers + .iter() + .find(|worker| worker.worker_index == 0) + .map(|worker| worker.worker_id) + .ok_or_else(|| to_mudu_error("server topology does not contain worker 0".to_string()))?; + let session_id = runtime + .block_on(client.create_session(mudu_contract::protocol::SessionCreateRequest::new(Some( + json!({ + "session_id": 0, + "worker_id": default_worker_id.to_string() + }) + .to_string(), + )))) + .map_err(|e| to_mudu_error(e.to_string()))? + .session_id(); + + invoke_void( + &runtime, + &mut client, + session_id, + "wallet/wallet/create_user", + (4i32, "Dave".to_string(), "dave@example.com".to_string()), + )?; + invoke_void(&runtime, &mut client, session_id, "wallet/wallet/delete_user", (4i32,))?; + assert_eq!( + query_row_count_via_client( + &runtime, + &mut client, + "wallet", + "SELECT user_id FROM users WHERE user_id = 4", + )?, + 0 + ); + assert_eq!( + query_row_count_via_client( + &runtime, + &mut client, + "wallet", + "SELECT user_id FROM wallets WHERE user_id = 4", + )?, + 0 + ); + assert_eq!( + query_row_count_via_client( + &runtime, + &mut client, + "wallet", + "SELECT user_id FROM users WHERE user_id = 1", + )?, + 1 + ); + assert_eq!( + query_i64_via_client( + &runtime, + &mut client, + "wallet", + "SELECT balance FROM wallets WHERE user_id = 1", + )?, + 10000 + ); + assert_eq!( + query_row_count_via_client(&runtime, &mut client, "wallet", "SELECT trans_id FROM transactions")?, + 0 + ); + assert!(runtime + .block_on(client.close_session( + mudu_contract::protocol::SessionCloseRequest::new(session_id) + )) + .map_err(|e| to_mudu_error(e.to_string()))? + .closed()); + + Ok(()) +} + +fn invoke_void( + runtime: &tokio::runtime::Runtime, + client: &mut AsyncClientImpl, + session_id: u128, + procedure_name: &str, + tuple: T, +) -> RS<()> { + let payload = serialize_param(tuple)?; + let result_binary = runtime + .block_on(client.invoke_procedure( + mudu_contract::protocol::ProcedureInvokeRequest::new( + session_id, + procedure_name.to_string(), + payload, + ), + )) + .map_err(|e| to_mudu_error(e.to_string()))? + .into_result(); + let result = procedure_invoke::deserialize_result(&result_binary)?; + let _: () = result.to(&<() as TupleDatum>::tuple_desc_static(&[]))?; + Ok(()) +} + +fn query_i64_via_client( + runtime: &tokio::runtime::Runtime, + client: &mut AsyncClientImpl, + app_name: &str, + sql: &str, +) -> RS { + let response = runtime + .block_on(client.query(mudu_contract::protocol::ClientRequest::new( + app_name.to_string(), + sql.to_string(), + ))) + .map_err(|e| to_mudu_error(e.to_string()))?; + let value = response + .rows() + .first() + .and_then(|row| row.values().first()) + .ok_or_else(|| to_mudu_error("query returned no rows".to_string()))?; + if let Some(v) = value.as_i64() { + Ok(*v) + } else if let Some(v) = value.as_i32() { + Ok(*v as i64) + } else if let Some(v) = value.as_string() { + v.parse::() + .map_err(|e| to_mudu_error(format!("parse integer result error: {e}"))) + } else { + Err(to_mudu_error("query returned non-integer value".to_string())) + } +} + +fn query_row_count_via_client( + runtime: &tokio::runtime::Runtime, + client: &mut AsyncClientImpl, + app_name: &str, + sql: &str, +) -> RS { + let response = runtime + .block_on(client.query(mudu_contract::protocol::ClientRequest::new( + app_name.to_string(), + sql.to_string(), + ))) + .map_err(|e| to_mudu_error(e.to_string()))?; + Ok(response.rows().len()) +} + +fn serialize_param(tuple: T) -> RS> { + let desc = T::tuple_desc_static(&[]); + let param = ProcedureParam::from_tuple(0, tuple, &desc)?; + procedure_invoke::serialize_param(param) +} + +fn to_mudu_error(message: String) -> mudu::error::err::MError { + mudu::m_error!(mudu::error::ec::EC::MuduError, message) +} + +struct RunningServer { + stop: Notifier, + handle: Option>>, +} + +impl Drop for RunningServer { + fn drop(&mut self) { + self.stop.notify_all(); + if let Some(handle) = self.handle.take() { + let join_result = handle.join().expect("join io_uring server thread"); + if let Err(err) = join_result { + panic!("io_uring server stopped with error: {err}"); + } + } + } +} + +struct TestContext { + server_mode: ServerMode, + http_port: u16, + pg_port: u16, + tcp_port: u16, + base_dir: PathBuf, + mpk_dir: PathBuf, + data_dir: PathBuf, +} + +impl TestContext { + fn new(server_mode: ServerMode) -> RS> { + let Some(http_port) = reserve_port()? else { + return Ok(None); + }; + let Some(pg_port) = reserve_port()? else { + return Ok(None); + }; + let Some(tcp_port) = reserve_port()? else { + return Ok(None); + }; + let base_dir = + std::env::temp_dir().join(format!("mududb-testing-{}", mudu_sys::random::uuid_v4())); + let mpk_dir = base_dir.join("mpk"); + let data_dir = base_dir.join("data"); + fs::create_dir_all(&mpk_dir).map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::IOErr, "create test mpk dir error", e) + })?; + fs::create_dir_all(&data_dir).map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::IOErr, "create test data dir error", e) + })?; + Ok(Some(Self { + server_mode, + http_port, + pg_port, + tcp_port, + base_dir, + mpk_dir, + data_dir, + })) + } + + fn start_server(&self) -> RS { + let cfg = self.build_cfg(); + let (stop, waiter) = notify_wait(); + let handle = thread::spawn(move || Backend::sync_serve_with_stop(cfg, waiter)); + wait_until_port_ready(self.http_port, "HTTP")?; + if self.server_mode == ServerMode::IOUring { + wait_until_port_ready(self.tcp_port, "TCP")?; + } + Ok(RunningServer { + stop, + handle: Some(handle), + }) + } + + fn build_cfg(&self) -> MuduDBCfg { + let mut cfg = MuduDBCfg::default(); + cfg.listen_ip = "127.0.0.1".to_string(); + cfg.http_listen_port = self.http_port; + cfg.pg_listen_port = self.pg_port; + cfg.tcp_listen_port = self.tcp_port; + cfg.http_worker_threads = 1; + cfg.io_uring_worker_threads = 2; + cfg.server_mode = self.server_mode; + cfg.routing_mode = RoutingMode::ConnectionId; + cfg.enable_async = true; + cfg.component_target = Some(ComponentTarget::P2); + cfg.mpk_path = self.mpk_dir.to_string_lossy().into_owned(); + cfg.db_path = self.data_dir.to_string_lossy().into_owned(); + cfg + } + + fn wallet_mpk_path(&self) -> PathBuf { + workspace_root() + .join("testing") + .join("mpk") + .join("wallet.mpk") + } + + fn http_addr(&self) -> String { + format!("127.0.0.1:{}", self.http_port) + } + + fn client_port(&self) -> u16 { + match self.server_mode { + ServerMode::Legacy => self.pg_port, + ServerMode::IOUring => self.tcp_port, + } + } + + fn wallet_db_path(&self) -> PathBuf { + self.data_dir.join("wallet") + } + + fn query_i64(&self, sql: &str) -> RS { + let value = self.query_first_value(sql)?; + match value { + LibsqlValue::Integer(value) => Ok(value), + other => Err(mudu::m_error!( + mudu::error::ec::EC::TypeErr, + format!("expected integer result, got {:?}", other) + )), + } + } + + fn query_string(&self, sql: &str) -> RS { + let value = self.query_first_value(sql)?; + match value { + LibsqlValue::Text(value) => Ok(value), + other => Err(mudu::m_error!( + mudu::error::ec::EC::TypeErr, + format!("expected text result, got {:?}", other) + )), + } + } + + fn query_first_value(&self, sql: &str) -> RS { + let db_path = self.wallet_db_path(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime"); + runtime.block_on(async move { + let db = Builder::new_local(db_path).build().await.map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::IOErr, "open wallet libsql db error", e) + })?; + let conn = db.connect().map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::IOErr, + "connect wallet libsql db error", + e + ) + })?; + let stmt = conn.prepare(sql).await.map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::MuduError, + "prepare wallet query error", + e + ) + })?; + let mut rows = stmt.query(params!()).await.map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::MuduError, + "execute wallet query error", + e + ) + })?; + let row = rows.next().await.map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::MuduError, "fetch wallet row error", e) + })?; + let row = row.ok_or_else(|| { + mudu::m_error!( + mudu::error::ec::EC::NoSuchElement, + "wallet query returned no rows" + ) + })?; + row.get_value(0).map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::MuduError, "read wallet value error", e) + }) + }) + } + + fn get_json(&self, path: &str) -> RS { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime"); + runtime.block_on(async { + let client = reqwest::Client::builder().no_proxy().build().map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::NetErr, "build http client error", e) + })?; + let url = format!("http://{}{}", self.http_addr(), path); + let response = + client.get(url).send().await.map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::NetErr, "GET request error", e) + })?; + let value = response.json::().await.map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::DecodeErr, + "decode GET response error", + e + ) + })?; + extract_http_data(value) + }) + } + + fn post_json(&self, path: &str, body: Value) -> RS { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime"); + runtime.block_on(async { + let client = reqwest::Client::builder().no_proxy().build().map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::NetErr, "build http client error", e) + })?; + let url = format!("http://{}{}", self.http_addr(), path); + let response = client.post(url).json(&body).send().await.map_err(|e| { + mudu::m_error!(mudu::error::ec::EC::NetErr, "POST request error", e) + })?; + let value = response.json::().await.map_err(|e| { + mudu::m_error!( + mudu::error::ec::EC::DecodeErr, + "decode POST response error", + e + ) + })?; + extract_http_data(value) + }) + } +} + +impl Drop for TestContext { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.base_dir); + } +} + +fn extract_http_data(response: Value) -> RS { + let status = response + .get("status") + .and_then(Value::as_i64) + .ok_or_else(|| { + mudu::m_error!( + mudu::error::ec::EC::DecodeErr, + "HTTP API response missing numeric status" + ) + })?; + if status == 0 { + return Ok(response.get("data").cloned().unwrap_or(Value::Null)); + } + let message = response + .get("message") + .and_then(Value::as_str) + .unwrap_or("HTTP API request failed"); + Err(mudu::m_error!( + mudu::error::ec::EC::MuduError, + format!( + "{}: {}", + message, + response.get("data").cloned().unwrap_or(Value::Null) + ) + )) +} + +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("testing crate has workspace root parent") + .to_path_buf() +}